From f1d694b47b6169af290d158e9b25d709e2b0cd49 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 18:58:42 -0500 Subject: [PATCH 001/395] 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 9e50bea6d3..ace6960615 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 da2ffbcabd..e00abb8d12 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 0000000000..62dc47b4e4 --- /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 8a4b16fc57..9400f7aa35 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 0000000000..ce9189a45a --- /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 0000000000..7d79b35c1e --- /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 0000000000..62dc47b4e4 --- /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 0000000000..e749a4b399 --- /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 0000000000..ae9673267f --- /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 0000000000..5da787ac2b --- /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 0000000000..2884558fb9 --- /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 0000000000..66d6b36ad4 --- /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 0000000000..5f7cbf5494 --- /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/395] 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 66d6b36ad4..0b4f9a161f 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 5f7cbf5494..aa20baaf45 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 0000000000..659015cc67 --- /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/395] 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 7d79b35c1e..e3cdfd077a 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 0000000000..14af8b85a5 --- /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 5da787ac2b..b70023c67f 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 2884558fb9..89c32d6b7b 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 0b4f9a161f..05d72c4be3 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 aa20baaf45..ba340df40a 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 0000000000..7e551bf2e3 --- /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 0000000000..4bf39c9417 --- /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 0000000000..3a9e39fbe4 --- /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 659015cc67..778ef152bc 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 d858d840f92d3239710ea2cd90724f001251ee27 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 08:46:44 -0500 Subject: [PATCH 004/395] Accept file URL paths in synchronous fs functions readFileSync, writeFileSync, appendFileSync, statSync, lstatSync, readdirSync, mkdirSync, rmSync, rmdirSync, mkdtempSync, accessSync, unlinkSync, chmodSync, chownSync, lchownSync, truncateSync, copyFileSync, renameSync, realpathSync, realpathSync.native, openSync and opendirSync accept a file: URL wherever they accept a path. The URL converts as fileURLToPath converts it, with Node's errors for other schemes, hosts, encoded separators and null bytes. Arguments after a URL path that have side effects are evaluated into hidden locals before the conversion, so they run even when it throws. A URL path after a string path, or with options that are not literals, fails with SC2020, because Node checks those arguments before it converts the URL. existsSync and the fs.promises functions also fail with SC2020 for URL paths: existsSync answers false for a URL it cannot convert, and fs.promises reports conversion errors as rejections. Fixes #1616 Co-authored-by: jlucaso1 <55464917+jlucaso1@users.noreply.github.com> --- docs/content/docs/limitations.mdx | 2 + .../builtin-apis/filesystem-url-paths.mdx | 3 + .../ambient/scriptc-node-fallback.d.ts | 81 +++---- .../lowering/builtins/filesystem-calls.ts | 17 +- .../frontend/lowering/builtins/filesystem.ts | 7 +- .../frontend/lowering/builtins/fs-paths.ts | 212 ++++++++++++++++++ .../lowering/builtins/module-calls.ts | 12 +- .../compiler/src/frontend/lowering/lowerer.ts | 22 +- .../tests/corpus/6931-fs-url-paths.ts.json | 6 + .../diagnostics/fs-url-path-fences.ts.json | 6 + .../node-types/fs-url-path-fences.ts.json | 6 + .../fixtures/node-types/fs-url-paths.ts.json | 6 + tests/corpus/6931-fs-url-paths.ts | 96 ++++++++ tests/diagnostics/fs-url-path-fences.ts | 15 ++ .../fixtures/node-types/fs-url-path-fences.ts | 9 + tests/fixtures/node-types/fs-url-paths.ts | 10 + .../__snapshots__/fs-url-path-fences.ts.txt | 35 +++ .../corpus-costs/6931-fs-url-paths.ts.json | 1 + tests/harness/project-config.test.ts | 29 +++ 19 files changed, 522 insertions(+), 53 deletions(-) create mode 100644 docs/content/limitations/builtin-apis/filesystem-url-paths.mdx create mode 100644 packages/compiler/src/frontend/lowering/builtins/fs-paths.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6931-fs-url-paths.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fs-url-path-fences.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-path-fences.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-paths.ts.json create mode 100644 tests/corpus/6931-fs-url-paths.ts create mode 100644 tests/diagnostics/fs-url-path-fences.ts create mode 100644 tests/fixtures/node-types/fs-url-path-fences.ts create mode 100644 tests/fixtures/node-types/fs-url-paths.ts create mode 100644 tests/harness/__snapshots__/fs-url-path-fences.ts.txt create mode 100644 tests/harness/corpus-costs/6931-fs-url-paths.ts.json diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index 05f9d329ac..5be38a9241 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -82,6 +82,8 @@ title: "Limitations" ../limitations/builtin-apis/filesystem-flags.mdx +../limitations/builtin-apis/filesystem-url-paths.mdx + ../limitations/builtin-apis/copy.mdx ../limitations/builtin-apis/glob.mdx diff --git a/docs/content/limitations/builtin-apis/filesystem-url-paths.mdx b/docs/content/limitations/builtin-apis/filesystem-url-paths.mdx new file mode 100644 index 0000000000..d5079ed5ae --- /dev/null +++ b/docs/content/limitations/builtin-apis/filesystem-url-paths.mdx @@ -0,0 +1,3 @@ +- **Filesystem URL paths:** Synchronous `fs` functions accept `file:` URL paths and convert them as `fileURLToPath` does. + `existsSync` and the `fs.promises` functions refuse URL paths; pass `fileURLToPath(url)` instead. + A URL path after a string path, or with options that are not literals, is refused, because Node checks those arguments before it converts the URL. diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index fb107ef994..ee7c794f16 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -1379,13 +1379,13 @@ interface ImportMeta { * `recursive`); empty directories go through `rmdirSync`. */ declare module "node:fs" { import { Readable, Writable } from "stream"; - export function readFileSync(path: string | number, encoding: BufferEncoding): string; + export function readFileSync(path: string | number | URL, encoding: BufferEncoding): string; export function readFileSync( - path: string | number, + path: string | number | URL, options: { encoding: BufferEncoding }, ): string; - export function readFileSync(path: string | number): Buffer; - export function readFileSync(path: string, options: unknown): string | Buffer; + export function readFileSync(path: string | number | URL): Buffer; + export function readFileSync(path: string | URL, options: unknown): string | Buffer; /* Error-first callbacks select string or Buffer data from their encoding. * Unknown options retain that union until the caller validates the value. */ export function readFile( @@ -1397,7 +1397,7 @@ declare module "node:fs" { * Node — an existing file keeps its permissions) and/or the utf8 * encoding spelling. */ export function writeFileSync( - path: string, + path: string | URL, data: string, /* The options-record stance (see http.RequestOptions): flag fences * by name at the call site; undocumented keys drop like Node. */ @@ -1409,14 +1409,17 @@ declare module "node:fs" { }, ): void; /* The bare-encoding spelling — the options record's encoding key alone. */ - export function writeFileSync(path: string, data: string, encoding: "utf8" | "utf-8"): void; - export function writeFileSync(path: string, data: string | Uint8Array): void; - export function appendFileSync(path: string, data: string | Uint8Array): void; + export function writeFileSync(path: string | URL, data: string, encoding: "utf8" | "utf-8"): void; + export function writeFileSync(path: string | URL, data: string | Uint8Array): void; + export function appendFileSync(path: string | URL, data: string | Uint8Array): void; export function existsSync(path: string): boolean; - export function mkdirSync(path: string): void; - export function mkdirSync(path: string, options: { recursive?: boolean; mode?: number }): void; - export function unlinkSync(path: string): void; - export function chmodSync(path: string, mode: number): void; + export function mkdirSync(path: string | URL): void; + export function mkdirSync( + path: string | URL, + options: { recursive?: boolean; mode?: number }, + ): void; + export function unlinkSync(path: string | URL): void; + export function chmodSync(path: string | URL, mode: number): void; export function fchmodSync(fd: number, mode: number): void; export function fsyncSync(fd: number): void; export function linkSync( @@ -1514,29 +1517,29 @@ declare module "node:fs" { path: string | Uint8Array | URL, options: string | { encoding?: string | null } | null, ): string | Buffer; - export function chownSync(path: string, uid: number, gid: number): void; - export function lchownSync(path: string, uid: number, gid: number): void; + export function chownSync(path: string | URL, uid: number, gid: number): void; + export function lchownSync(path: string | URL, uid: number, gid: number): void; export function fchownSync(fd: number, uid: number, gid: number): void; - export function truncateSync(path: string, len?: number): void; + export function truncateSync(path: string | URL, len?: number): void; /* The 2-argument form only (Node's mode flags have no lowering). The * destination is created or truncated carrying the SOURCE's mode. */ - export function copyFileSync(src: string, dest: string): void; + export function copyFileSync(src: string | URL, dest: string | URL): void; export function rename( oldPath: string, newPath: string, callback: (err: NodeJS.ErrnoException | null) => void, ): void; - export function renameSync(oldPath: string, newPath: string): void; - export function rmSync(path: string): void; + export function renameSync(oldPath: string | URL, newPath: string | URL): void; + export function rmSync(path: string | URL): void; /* maxRetries/retryDelay are accepted no-ops (Node retries around * EBUSY-class races; the synchronous lowering has no re-entrancy that * needs them). */ export function rmSync( - path: string, + path: string | URL, options: { recursive?: boolean; force?: boolean; maxRetries?: number; retryDelay?: number }, ): void; - export function rmdirSync(path: string): void; - export function readdirSync(path: string): string[]; + export function rmdirSync(path: string | URL): void; + export function readdirSync(path: string | URL): string[]; /* The withFileTypes form: Dirent rows (name + parentPath + the type * probes — the honest subset of Node's Dirent surface). The type is * interned explicitly in type-mapper.ts (the record's hidden %dtype field @@ -1550,7 +1553,7 @@ declare module "node:fs" { isSymbolicLink(): boolean; } export function readdirSync( - path: string, + path: string | URL, /* The options-record stance (see http.RequestOptions): recursive * fences by name at the call site; undocumented keys drop like Node. */ options: { withFileTypes: true; recursive?: boolean; [option: string]: unknown }, @@ -1599,7 +1602,7 @@ declare module "node:fs" { encoding?: "utf8" | "utf-8"; bufferSize?: number; } - export function opendirSync(path: string, options?: OpenDirOptions): Dir; + export function opendirSync(path: string | URL, options?: OpenDirOptions): Dir; /* fs.ReadStream / fs.WriteStream: runtime Readable and Writable streams * over a file (scr_fs_stream.c). The options are checked at runtime * with Node's rules; flags and mode are checked when the file opens, so @@ -1641,8 +1644,8 @@ declare module "node:fs" { path: string | Buffer | URL, options?: BufferEncoding | WriteStreamOptions, ): WriteStream; - export function accessSync(path: string, mode?: number): void; - export function mkdtempSync(prefix: string): string; + export function accessSync(path: string | URL, mode?: number): void; + export function mkdtempSync(prefix: string | URL): string; export const constants: { readonly F_OK: number; readonly R_OK: number; @@ -1745,34 +1748,34 @@ declare module "node:fs" { bigint?: boolean; throwIfNoEntry?: boolean; } - export function statSync(path: string, options?: undefined): Stats; + export function statSync(path: string | URL, options?: undefined): Stats; export function statSync( - path: string, + path: string | URL, options: StatSyncOptions & { bigint?: false; throwIfNoEntry: false }, ): Stats | undefined; export function statSync( - path: string, + path: string | URL, options: StatSyncOptions & { bigint?: false; throwIfNoEntry?: true }, ): Stats; /* bigint: true type-checks so the lowering can name it as unsupported. */ - export function statSync(path: string, options: StatSyncOptions & { bigint: true }): Stats; - export function statSync(path: string, options: StatSyncOptions): Stats | undefined; - export function lstatSync(path: string, options?: undefined): Stats; + export function statSync(path: string | URL, options: StatSyncOptions & { bigint: true }): Stats; + export function statSync(path: string | URL, options: StatSyncOptions): Stats | undefined; + export function lstatSync(path: string | URL, options?: undefined): Stats; export function lstatSync( - path: string, + path: string | URL, options: StatSyncOptions & { bigint?: false; throwIfNoEntry: false }, ): Stats | undefined; export function lstatSync( - path: string, + path: string | URL, options: StatSyncOptions & { bigint?: false; throwIfNoEntry?: true }, ): Stats; /* bigint: true type-checks so the lowering can name it as unsupported. */ - export function lstatSync(path: string, options: StatSyncOptions & { bigint: true }): Stats; - export function lstatSync(path: string, options: StatSyncOptions): Stats | undefined; + export function lstatSync(path: string | URL, options: StatSyncOptions & { bigint: true }): Stats; + export function lstatSync(path: string | URL, options: StatSyncOptions): Stats | undefined; /* The fd pair behind spawn's fd-stdio form: openSync(path, flags) → * the raw fd. Numeric flags use an inline OR of the O_* constants. */ - export function openSync(path: string, flags: string): number; - export function openSync(path: string, flags: number, mode?: number): number; + export function openSync(path: string | URL, flags: string): number; + export function openSync(path: string | URL, flags: number, mode?: number): number; export function closeSync(fd: number): void; export function fdatasyncSync(fd: number): void; export function ftruncateSync(fd: number, len?: number): void; @@ -1853,9 +1856,9 @@ declare module "node:fs" { export function fstatSync(fd: number): Stats; /* realpath(3) — resolves symlinks, `.`/`..`, throwing Node's fs error * shapes for missing paths. */ - export function realpathSync(path: string): string; + export function realpathSync(path: string | URL): string; export namespace realpathSync { - function native(path: string): string; + function native(path: string | URL): string; } /* statfs(2)/statvfs(3) — the filesystem-capacity snapshot (the fields * Node's statfsSync reports; bavail × bsize is the free-space probe). */ diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts index 05028e1dfd..f23dff0293 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts @@ -1,6 +1,7 @@ import { dynUndefinedExpr, boolLit, numLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; +import { lowerFsPath } from "./fs-paths.js"; import { locOf } from "../../program.js"; import { type BuiltinModuleFn, @@ -231,7 +232,7 @@ export function lowerFsNumericOpenCall( "use an inline bitwise OR of fs.constants.O_RDONLY/O_WRONLY/O_RDWR/O_CREAT/O_EXCL/O_NOFOLLOW/O_NONBLOCK/O_TRUNC/O_APPEND", ); } - const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!); const mode = expr.arguments.nodes[2] ? lowerer.lowerExprExpecting(expr.arguments.nodes[2]!, F64) : ({ kind: "numLit", value: 0o666, type: F64, loc } satisfies IrExpr); @@ -640,7 +641,9 @@ export function lowerFsEncodedRead( return null; } const sourceType = descriptor ? F64 : STRING; - const source = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, sourceType); + const source = descriptor + ? lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, sourceType) + : lowerFsPath(lowerer, expr.arguments.nodes[0]!); const encoding: IrExpr = optionsType.kind === "string" ? lowerer.lowerExprExpecting(options, STRING) @@ -742,7 +745,7 @@ export function lowerFsOpendirCall( ) { lowerer.noLowering(`${member} with ${expr.arguments.nodes.length} arguments`, expr); } - const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!); let bufferSize: IrExpr = { kind: "numLit", value: 32, type: F64, loc }; const optsNode = expr.arguments.nodes[1]; if (optsNode && !(ts.isIdentifier(optsNode) && optsNode.text === "undefined")) { @@ -812,7 +815,7 @@ export function lowerFsMkdirSyncCall( ); } const { recursive, modeNode } = options; - const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!); const mode: IrExpr | null = modeNode ? lowerer.lowerExprExpecting(modeNode, F64) : null; if (!recursive) { return mode @@ -884,7 +887,7 @@ export function lowerFsRemoveCall(lowerer: Lowerer, expr: ts.CallExpression, loc "the recursive/force flags must be boolean literals; maxRetries/retryDelay are numbers; other options have no lowering", ); } - const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!); const recursive = boolLit(opts.bools["recursive"] === true, loc); const force = boolLit(opts.bools["force"] === true, loc); @@ -941,7 +944,7 @@ export function lowerFsWriteBytesCall( "byte writes take Uint8Array/Buffer data", ); } - const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!); const data = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, BYTES_U8); if (bi.module === "fs/promises") { return { @@ -981,7 +984,7 @@ export function lowerFsWriteOptionsCall( // values in object-literal order before issuing the write; otherwise a // call/getter statically typed as the accepted utf8 literal can vanish. const finishWrite = (optionValues: WriteOptionValue[]): IrExpr => { - const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!); const data = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, STRING); const pathLocal = lowerer.declareHiddenLocal("%writePath", STRING); const dataLocal = lowerer.declareHiddenLocal("%writeData", STRING); diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index 49875e004c..0e1c062c50 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -1,6 +1,7 @@ import { dynUndefinedExpr, boolLit, numLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, ladderFenceExpr } from "../lowerer.js"; +import { lowerFsPath } from "./fs-paths.js"; import { isJsSourceFile, locOf } from "../../program.js"; import { builtinModuleFnOf, @@ -513,7 +514,7 @@ export function lowerStringOrBytesWrite( kind: "call", callee: helper, args: [ - lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING), + lowerFsPath(lowerer, call.arguments.nodes[0]!), lowerer.lowerExprExpecting(call.arguments.nodes[1]!, dataType), ], type: VOID, @@ -1376,7 +1377,7 @@ export function lowerFsReaddirTypesCall( ) { fence(); } - const path = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); + const path = lowerFsPath(lowerer, call.arguments.nodes[0]!); return { kind: "libCall", fn: promiseForm ? "fsp.readdirTypes" : "fs.readdirTypesSync", @@ -1405,7 +1406,7 @@ export function lowerFsStatOptionsCall( const spelling = noFollow ? "lstatSync" : "statSync"; const plainFn: IrLibFn = noFollow ? "fs.lstatSync" : "fs.statSync"; const optsNode = call.arguments.nodes[1]!; - const path = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); + const path = lowerFsPath(lowerer, call.arguments.nodes[0]!); // The path binds first, then the options' effects, then the stat runs. const plainAfter = (effect: IrExpr | null): IrExpr => { if (effect === null) return { kind: "libCall", fn: plainFn, args: [path], type: STATS_T, loc }; diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts b/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts new file mode 100644 index 0000000000..fb9176b53d --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts @@ -0,0 +1,212 @@ +/* fs path arguments given as file: URLs. Node converts a URL path with + * fileURLToPath inside each function (getValidatedPath), after every + * argument has been evaluated, and validates some arguments before the + * path and others after it. A URL-typed path lowers to the same + * conversion (url.fileURLToPathUrl). Later arguments with side effects are + * evaluated into hidden locals before the conversion, so they run even + * when it throws. The conversion keeps Node's error order when every + * earlier path is a URL too and each argument Node validates before the + * path is absent or a literal that passes that validation; other URL + * forms fence with SC2020, as do the fs.promises functions, whose + * conversion errors are rejections rather than throws, and existsSync, + * which answers false for a URL it cannot convert. */ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { locOf } from "../../program.js"; +import { sideEffectFreeOptionValue } from "../surfaces.js"; +import { type IrExpr, type IrStmt, STRING, URL_T } from "../../../ir/ir.js"; + +/** The fs functions whose path parameters accept a URL: the path + * parameter indexes, and whether Node validates the paths before every + * other argument (otherwise the options come first). */ +const FS_URL_PATHS: Readonly< + Record +> = { + accessSync: { paths: [0], pathsFirst: true }, + appendFileSync: { paths: [0], pathsFirst: false }, + chmodSync: { paths: [0], pathsFirst: true }, + chownSync: { paths: [0], pathsFirst: true }, + copyFileSync: { paths: [0, 1], pathsFirst: true }, + lchownSync: { paths: [0], pathsFirst: true }, + lstatSync: { paths: [0], pathsFirst: true }, + mkdirSync: { paths: [0], pathsFirst: false }, + mkdtempSync: { paths: [0], pathsFirst: false }, + openSync: { paths: [0], pathsFirst: true }, + opendirSync: { paths: [0], pathsFirst: true }, + readFileSync: { paths: [0], pathsFirst: false }, + readdirSync: { paths: [0], pathsFirst: false }, + realpathSync: { paths: [0], pathsFirst: false }, + "realpathSync.native": { paths: [0], pathsFirst: false }, + renameSync: { paths: [0, 1], pathsFirst: true }, + rmSync: { paths: [0], pathsFirst: false }, + rmdirSync: { paths: [0], pathsFirst: true }, + statSync: { paths: [0], pathsFirst: true }, + truncateSync: { paths: [0], pathsFirst: true }, + unlinkSync: { paths: [0], pathsFirst: true }, + writeFileSync: { paths: [0], pathsFirst: false }, +}; + +/** The fs.promises functions with path parameters, by path indexes. */ +const FSP_PATHS: Readonly> = { + access: [0], + appendFile: [0], + chmod: [0], + chown: [0], + copyFile: [0, 1], + lchown: [0], + lstat: [0], + mkdir: [0], + mkdtemp: [0], + open: [0], + opendir: [0], + readFile: [0], + readdir: [0], + realpath: [0], + rename: [0, 1], + rm: [0], + rmdir: [0], + stat: [0], + truncate: [0], + unlink: [0], + writeFile: [0], +}; + +function isUrlArgument(lowerer: Lowerer, node: ts.Expression | undefined): boolean { + return node !== undefined && lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "url"; +} + +/** A literal that Node's option validation accepts before it reads the + * path: a string (the checker admits only the declared encodings, flags + * and type names), a boolean, an integer in [0, 2^31 - 1] (every numeric + * option of these functions accepts that range), undefined or null, or an + * object literal of those. */ +function preValidatedLiteral(node: ts.Expression): boolean { + let e = node; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (ts.isStringLiteralLike(e)) return true; + if ( + e.kind === ts.SyntaxKind.TrueKeyword || + e.kind === ts.SyntaxKind.FalseKeyword || + e.kind === ts.SyntaxKind.NullKeyword + ) + return true; + if (ts.isIdentifier(e)) return e.text === "undefined"; + if (ts.isNumericLiteral(e)) { + const value = Number(e.text); + return Number.isInteger(value) && value >= 0 && value <= 2147483647; + } + if (ts.isObjectLiteralExpression(e)) { + return e.properties.nodes.every( + (p) => + ts.isPropertyAssignment(p) && + !ts.isComputedPropertyName(p.name) && + preValidatedLiteral(p.initializer), + ); + } + return false; +} + +/** SC2020 when the call passes a URL path in a form whose conversion + * order would differ from Node's (see the file comment). */ +function checkUrlPaths( + lowerer: Lowerer, + call: ts.CallExpression, + module: string, + member: string, +): void { + const args = call.arguments.nodes; + const name = module === "fs/promises" ? `fs.promises.${member}` : member; + const hint = "convert the URL first: fileURLToPath(url) from node:url"; + if (module === "fs/promises") { + if ((FSP_PATHS[member] ?? []).some((i) => isUrlArgument(lowerer, args[i]))) + lowerer.noLowering(`${name} with a URL path`, call, hint); + return; + } + if (member === "existsSync" && isUrlArgument(lowerer, args[0])) + lowerer.noLowering(`${name} with a URL path`, call, hint); + const row = FS_URL_PATHS[member]; + if (!row) return; + const first = row.paths.find((i) => isUrlArgument(lowerer, args[i])); + if (first === undefined) return; + if (args.some(ts.isSpreadElement)) + lowerer.noLowering(`${name} with spread arguments and a URL path`, call); + // The runtime checks a string path after the conversion, so a string + // path Node checks first cannot precede the URL. + if (row.paths.some((i) => i < first && args[i] !== undefined)) + lowerer.noLowering(`${name} with a string path before a URL path`, call, hint); + const staged: ts.Expression[] = []; + for (let i = first + 1; i < args.length; i++) { + const arg = args[i]!; + if (sideEffectFreeOptionValue(arg)) continue; + // Literals are read by their members, so a whole-value slot cannot + // hold them. + let e = arg; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (ts.isObjectLiteralExpression(e) || ts.isArrayLiteralExpression(e)) + lowerer.noLowering(`${name} with a URL path and a later literal that has effects`, arg, hint); + staged.push(e); + } + if (staged.length > 0) { + lowerer.stagedAfterPath.set(args[first]!, staged); + for (const e of staged) lowerer.stagedArguments.set(e, null); + } + if (row.pathsFirst) return; + // writeFileSync/appendFileSync's data is checked after the path. + const data = member === "writeFileSync" || member === "appendFileSync" ? 1 : -1; + for (let i = 0; i < args.length; i++) { + if (row.paths.includes(i) || i === data || preValidatedLiteral(args[i]!)) continue; + lowerer.noLowering(`${name} with a URL path and options that are not literals`, args[i]!, hint); + } +} + +/** Fences URL paths that cannot convert in Node's order: the check every + * fs and fs.promises call runs before lowering its arguments. */ +export function checkFsUrlPaths( + lowerer: Lowerer, + call: ts.CallExpression, + bi: { module: string; member: string }, +): void { + if (bi.module === "fs" || bi.module === "fs/promises") + checkUrlPaths(lowerer, call, bi.module, bi.member); +} + +/** An fs path argument as a string: a URL converts with fileURLToPath's + * conversion (url.fileURLToPathUrl), which throws Node's errors for other + * schemes, hosts and encoded separators. */ +export function lowerFsPath(lowerer: Lowerer, node: ts.Expression): IrExpr { + if (!isUrlArgument(lowerer, node)) return lowerer.lowerExprExpecting(node, STRING); + const loc = locOf(node); + const url = lowerer.lowerExprExpecting(node, URL_T); + const staged = lowerer.stagedAfterPath.get(node); + if (staged === undefined) + return { kind: "libCall", fn: "url.fileURLToPathUrl", args: [url], type: STRING, loc }; + // The URL and the later arguments with effects, in source order, then + // the conversion. + const saved = lowerer.declareHiddenLocal("%fsUrl", URL_T); + const stmts: IrStmt[] = [{ kind: "varDecl", localId: saved.id, init: url, loc }]; + for (const arg of staged) { + lowerer.stagedArguments.delete(arg); + const value = lowerer.lowerExpr(arg); + const slot = lowerer.declareHiddenLocal("%fsArg", value.type); + stmts.push({ kind: "varDecl", localId: slot.id, init: value, loc: locOf(arg) }); + lowerer.stagedArguments.set(arg, { localId: slot.id, type: value.type }); + } + return { + kind: "seqExpr", + stmts, + result: { + kind: "libCall", + fn: "url.fileURLToPathUrl", + args: [{ kind: "varRef", localId: saved.id, type: URL_T, loc }], + type: STRING, + loc, + }, + type: STRING, + loc, + }; +} + +/** Whether `index` is a path parameter of the fs function `member`. */ +export function isFsPathParam(member: string, index: number): boolean { + return FS_URL_PATHS[member]?.paths.includes(index) === true; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts index c85f2c7770..d33585d83e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts @@ -25,6 +25,7 @@ import { lowerFsWriteOptionsCall, lowerFsReadPathCall, } from "./filesystem-calls.js"; +import { checkFsUrlPaths, isFsPathParam, lowerFsPath } from "./fs-paths.js"; import { lowerBufferEncodingCall } from "./text-codecs.js"; import { lowerUtilTypeCall } from "./util-types.js"; import { InternalCompilerError } from "../../../errors.js"; @@ -58,6 +59,7 @@ export function lowerBuiltinModuleCall( loc: SrcLoc, ): IrExpr { const name = ts.getTextOfNode(expr.expression); + checkFsUrlPaths(lowerer, expr, bi); if (bi.module === "util/types") { return lowerUtilTypeCall(lowerer, expr, bi, loc, name); } @@ -240,7 +242,7 @@ export function lowerBuiltinModuleCall( expr.arguments.nodes.length === 1 && lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind !== "f64" ) { - const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!); return { kind: "libCall", fn: "fs.readFileSyncBytes", args: [path], type: BYTES_U8, loc }; } if ( @@ -305,7 +307,7 @@ export function lowerBuiltinModuleCall( if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { lowerer.noLowering(`${bi.member} with ${expr.arguments.nodes.length} arguments`, expr); } - const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!); const second: IrExpr = expr.arguments.nodes[1] ? lowerer.lowerExprExpecting(expr.arguments.nodes[1], F64) : { kind: "numLit", value: 0, type: F64, loc }; @@ -467,7 +469,11 @@ export function lowerBuiltinModuleCall( ); args = lowerer.completeArgs(expr.arguments.nodes, shapes, loc, expr); } else { - args = expr.arguments.nodes.map((a, i) => lowerer.lowerExprExpecting(a, fn.params[i])); + args = expr.arguments.nodes.map((a, i) => + bi.module === "fs" && isFsPathParam(bi.member, i) + ? lowerFsPath(lowerer, a) + : lowerer.lowerExprExpecting(a, fn.params[i]), + ); for (let i = args.length; i < fn.params.length; i++) { const dflt = fn.defaults![i - required]!; args.push({ kind: "strLit", value: dflt, type: STRING, loc }); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 543149f835..80b2a59389 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2918,6 +2918,12 @@ export class Lowerer { * for the duration of that call, the receiver lowers to the Transform of * the value held in the named local. */ readonly hashStreamReceivers = new Map(); + /** fs call arguments evaluated ahead of a URL path's conversion + * (builtins/fs-paths.ts): each lowers to the hidden local that holds its + * value, which is null until the path's lowering has saved it. */ + readonly stagedArguments = new Map(); + /** The staged arguments that follow each such URL path, in order. */ + readonly stagedAfterPath = new Map(); readonly moduleNsParamOverrides = new Map< ts.ParameterDeclaration, IrType & { kind: "moduleNs" } @@ -10003,6 +10009,8 @@ export class Lowerer { // retain their own checked conversion and must not be erased here. while (ts.isParenthesizedExpression(node) || ts.isSatisfiesExpression(node)) node = node.expression; + const staged = this.stagedArgument(node); + if (staged) return expected ? this.coerceInto(node, staged, expected) : staged; if (expected) { if (ts.isConditionalExpression(node)) return lowerTernary(this, node, expected); const escaped = this.neverAssertionInto(node, expected); @@ -12035,7 +12043,19 @@ export class Lowerer { lowerExpr(expr: ts.Expression): IrExpr { const hash = this.hashStreamReceivers.get(expr); if (hash) return hashStreamOf(varRef(hash.localId, hash.type, locOf(expr))); - return lowerExpr(this, expr); + return this.stagedArgument(expr) ?? lowerExpr(this, expr); + } + + /** The saved value of a staged fs argument (stagedArguments), or null for + * every other expression. */ + private stagedArgument(node: ts.Expression): IrExpr | null { + if (this.stagedArguments.size === 0) return null; + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (!this.stagedArguments.has(node)) return null; + const slot = this.stagedArguments.get(node); + if (!slot) + throw new InternalCompilerError("an fs argument lowered before the URL path it follows"); + return varRef(slot.localId, slot.type, locOf(node)); } lowerIntrinsicProperty(expr: ts.PropertyAccessExpression): IrExpr | null { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6931-fs-url-paths.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6931-fs-url-paths.ts.json new file mode 100644 index 0000000000..6a1574e04a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6931-fs-url-paths.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6931-fs-url-paths.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fs-url-path-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fs-url-path-fences.ts.json new file mode 100644 index 0000000000..c25c78ec00 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/fs-url-path-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/fs-url-path-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-path-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-path-fences.ts.json new file mode 100644 index 0000000000..7b63a55641 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-path-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/fs-url-path-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-paths.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-paths.ts.json new file mode 100644 index 0000000000..cdc4ee43f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-paths.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/fs-url-paths.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6931-fs-url-paths.ts b/tests/corpus/6931-fs-url-paths.ts new file mode 100644 index 0000000000..91a630d1bb --- /dev/null +++ b/tests/corpus/6931-fs-url-paths.ts @@ -0,0 +1,96 @@ +// fs functions given file: URL paths: the URL converts as fileURLToPath +// converts it, so names with spaces and non-ASCII characters reach the same +// file, and URLs with another scheme, a host or an encoded separator throw +// Node's errors. +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { pathToFileURL } from "node:url"; + +const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "fs-url-"))); +const at = (name: string): URL => pathToFileURL(path.join(dir, name)); +// Arguments after a URL path are evaluated before the URL converts. +function logged(name: string): URL { + console.log("evaluated", name); + return at(name); +} +function mode(): number { + console.log("evaluated mode"); + return 0o600; +} + +function clean(text: string): string { + return text.split(dir).join(""); +} + +function attempt(label: string, run: () => void): void { + try { + run(); + console.log(label, "returned"); + } catch (error) { + if (error instanceof Error) { + const code = (error as NodeJS.ErrnoException).code; + console.log(label, error.name, code, clean(error.message)); + } + } +} + +const file = at("a b ü.txt"); +fs.writeFileSync(file, "hello"); +console.log(fs.readFileSync(file, "utf8")); +console.log(fs.readFileSync(file).length); +console.log(fs.readFileSync(file, { encoding: "utf8" })); +console.log(fs.readFileSync(file, "base64")); +fs.appendFileSync(file, " world"); +fs.writeFileSync(at("bytes.bin"), new Uint8Array([1, 2, 3])); +fs.writeFileSync(at("mode.txt"), "m", { mode: 0o600 }); +console.log(fs.readFileSync(path.join(dir, "a b ü.txt"), "utf8")); +console.log(fs.statSync(file).size, fs.lstatSync(file).isFile(), fs.statSync(at("bytes.bin")).size); +console.log(fs.statSync(at("missing"), { throwIfNoEntry: false }) === undefined); +fs.accessSync(file); +fs.accessSync(file, fs.constants.R_OK); +fs.chmodSync(file, 0o644); +console.log((fs.statSync(file).mode & 0o777).toString(8)); + +fs.mkdirSync(at("sub/deep"), { recursive: true }); +fs.mkdirSync(at("plain")); +console.log(fs.readdirSync(at("sub"))); +console.log(fs.readdirSync(at("sub"), { withFileTypes: true }).map((entry) => entry.name)); +fs.copyFileSync(file, at("sub/copy.txt")); +fs.renameSync(at("sub/copy.txt"), at("sub/moved.txt")); +console.log(fs.readFileSync(at("sub/moved.txt"), "utf8")); +fs.truncateSync(at("sub/moved.txt"), 4); +console.log(fs.readFileSync(at("sub/moved.txt"), "utf8")); +const fd = fs.openSync(at("sub/moved.txt"), "r"); +console.log(typeof fd); +fs.closeSync(fd); +console.log(clean(fs.realpathSync(at("sub/../sub/moved.txt")))); +console.log(clean(fs.realpathSync.native(at("sub")))); +const temp = fs.mkdtempSync(at("tmp-")); +console.log(path.dirname(temp) === dir, path.basename(temp).length); +fs.rmdirSync(pathToFileURL(temp)); +const opened = fs.opendirSync(at("sub")); +console.log(opened.path === path.join(dir, "sub")); +opened.closeSync(); +fs.unlinkSync(at("sub/moved.txt")); +fs.rmSync(at("sub"), { recursive: true, force: true }); +fs.rmSync(at("plain"), { recursive: true }); +console.log(fs.readdirSync(dir).sort()); + +attempt("scheme", () => fs.readFileSync(new URL("http://example.com/a.txt"), "utf8")); +attempt("host", () => fs.statSync(new URL("file://example.com/a.txt"))); +attempt("separator", () => fs.readFileSync(new URL("file:///tmp/a%2Fb"), "utf8")); +attempt("lowercase separator", () => fs.unlinkSync(new URL("file:///tmp/a%2fb"))); +attempt("null byte", () => fs.readFileSync(new URL("file:///tmp/a%00b"))); +attempt("missing", () => fs.readFileSync(at("missing.txt"), "utf8")); +attempt("both bad", () => fs.copyFileSync(new URL("http://a/"), new URL("file://b/c"))); +attempt("rename", () => fs.renameSync(at("missing.txt"), at("other.txt"))); +attempt("mkdir", () => { + fs.mkdirSync(new URL("ftp://a/b"), { recursive: true }); +}); +attempt("later effects", () => fs.copyFileSync(new URL("http://a/"), logged("dest.txt"))); +attempt("later mode", () => fs.chmodSync(new URL("http://a/"), mode())); +attempt("chmod", () => fs.chmodSync(new URL("data:text/plain,a"), 0o600)); + +fs.rmSync(dir, { recursive: true, force: true }); +console.log("done", fs.existsSync(dir)); diff --git a/tests/diagnostics/fs-url-path-fences.ts b/tests/diagnostics/fs-url-path-fences.ts new file mode 100644 index 0000000000..080c561e1c --- /dev/null +++ b/tests/diagnostics/fs-url-path-fences.ts @@ -0,0 +1,15 @@ +// URL paths whose conversion would run in a different order than Node's. +import * as fs from "node:fs"; + +const url = new URL("file:///tmp/fs-url-path-fences"); +const encoding: BufferEncoding = process.argv.length > 9 ? "latin1" : "utf8"; +const recursive = process.argv.length > 9; + +// Node validates the string source before converting the URL destination. +fs.copyFileSync("/tmp/source", url); +// Node validates the options before the path. +console.log(fs.readFileSync(url, encoding)); +fs.mkdirSync(url, { recursive }); +// A literal's members are lowered one by one, so they cannot be evaluated +// ahead of the conversion as one value. +console.log(fs.statSync(url, { throwIfNoEntry: process.argv.length > 9 })); diff --git a/tests/fixtures/node-types/fs-url-path-fences.ts b/tests/fixtures/node-types/fs-url-path-fences.ts new file mode 100644 index 0000000000..4ae142189f --- /dev/null +++ b/tests/fixtures/node-types/fs-url-path-fences.ts @@ -0,0 +1,9 @@ +import { existsSync } from "node:fs"; +import { readFile, stat } from "node:fs/promises"; + +const url = new URL("file:///tmp/node-types-url-fences"); +// existsSync answers false for a URL it cannot convert. +console.log(existsSync(url)); +// The fs.promises functions reject for a URL they cannot convert. +void readFile(url, "utf8"); +void stat(url); diff --git a/tests/fixtures/node-types/fs-url-paths.ts b/tests/fixtures/node-types/fs-url-paths.ts new file mode 100644 index 0000000000..6d33d708d1 --- /dev/null +++ b/tests/fixtures/node-types/fs-url-paths.ts @@ -0,0 +1,10 @@ +import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; + +const dir = mkdtempSync(join(tmpdir(), "node-types-url-")); +const file = pathToFileURL(join(dir, "a b.txt")); +writeFileSync(file, "hello"); +console.log(readFileSync(file, "utf8"), readFileSync(file).length, statSync(file).size); +rmSync(pathToFileURL(dir), { recursive: true }); diff --git a/tests/harness/__snapshots__/fs-url-path-fences.ts.txt b/tests/harness/__snapshots__/fs-url-path-fences.ts.txt new file mode 100644 index 0000000000..1a53f8df5a --- /dev/null +++ b/tests/harness/__snapshots__/fs-url-path-fences.ts.txt @@ -0,0 +1,35 @@ +fs-url-path-fences.ts:9:1 - error SC2020: 'copyFileSync with a string path before a URL path' is part of the standard library types but has no scriptc lowering yet + + 8 | // Node validates the string source before converting the URL destination. + 9 | fs.copyFileSync("/tmp/source", url); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 10 | // Node validates the options before the path. + + hint: convert the URL first: fileURLToPath(url) from node:url + +fs-url-path-fences.ts:11:34 - error SC2020: 'readFileSync with a URL path and options that are not literals' is part of the standard library types but has no scriptc lowering yet + + 10 | // Node validates the options before the path. + 11 | console.log(fs.readFileSync(url, encoding)); + | ^~~~~~~~ + 12 | fs.mkdirSync(url, { recursive }); + + hint: convert the URL first: fileURLToPath(url) from node:url + +fs-url-path-fences.ts:12:19 - error SC2020: 'mkdirSync with a URL path and options that are not literals' is part of the standard library types but has no scriptc lowering yet + + 11 | console.log(fs.readFileSync(url, encoding)); + 12 | fs.mkdirSync(url, { recursive }); + | ^~~~~~~~~~~~~ + 13 | // A literal's members are lowered one by one, so they cannot be evaluated + + hint: convert the URL first: fileURLToPath(url) from node:url + +fs-url-path-fences.ts:15:30 - error SC2020: 'statSync with a URL path and a later literal that has effects' is part of the standard library types but has no scriptc lowering yet + + 14 | // ahead of the conversion as one value. + 15 | console.log(fs.statSync(url, { throwIfNoEntry: process.argv.length > 9 })); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 16 | + + hint: convert the URL first: fileURLToPath(url) from node:url \ No newline at end of file diff --git a/tests/harness/corpus-costs/6931-fs-url-paths.ts.json b/tests/harness/corpus-costs/6931-fs-url-paths.ts.json new file mode 100644 index 0000000000..458abd256d --- /dev/null +++ b/tests/harness/corpus-costs/6931-fs-url-paths.ts.json @@ -0,0 +1 @@ +{ "plain": 4139, "san": 50584 } diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index c7a5202f2a..dabb9b0bfc 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -764,6 +764,35 @@ test("node-types: exec's numeric ExecException.code remains a named fence", asyn } }); +test("node-types: fs functions take URL paths under @types/node's PathLike", async () => { + const dir = outDirFor("node-fs-url-paths"); + const result = await compile(join(nodeTypesDir, "fs-url-paths.ts"), { + outPath: join(dir, "fs-url-paths"), + outDir: dir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const { stdout } = await execFileAsync(result.binaryPath); + expect(stdout).toBe("hello 5 5\n"); +}); + +test("node-types: existsSync and fs.promises URL paths remain named fences", async () => { + const dir = outDirFor("node-fs-url-path-fences"); + const result = await compile(join(nodeTypesDir, "fs-url-path-fences.ts"), { + outPath: join(dir, "fs-url-path-fences"), + outDir: dir, + sanitize, + }); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.diagnostics.map((diagnostic) => [diagnostic.code, diagnostic.message])).toEqual([ + ["SC2020", expect.stringContaining("'existsSync with a URL path'")], + ["SC2020", expect.stringContaining("'fs.promises.readFile with a URL path'")], + ["SC2020", expect.stringContaining("'fs.promises.stat with a URL path'")], + ]); +}); + test("node-types: path and os lower statically under @types/node's shapes", async () => { const outDir = outDirFor("node-path-os"); const result = await compile(join(nodeTypesDir, "path-os.ts"), { From 9a73f63b0655b8180c23a86bdbb970b1af69314b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 12:17:22 -0500 Subject: [PATCH 005/395] Pass undefined as the zlib callback result on errors Node calls a zlib callback with (err, undefined) when the operation fails. The callback adapter passed a NULL Buffer in the result's static Buffer slot instead: member reads crashed and typeof printed "object". This applied to deflate, inflate, deflateRaw, inflateRaw, gzip, gunzip and unzip, with and without options, for callback literals and for function values. A function shaped (err, out), with err typed Error | null and out a Buffer that the body reads, now tests out on entry (bytes.isAbsent). With a Buffer, the body runs as before. With the NULL Buffer, a second copy of the body runs with out read as any holding undefined, so typeof, comparisons, templates, optional chains and console.log give Node's answers and member reads throw Node's TypeError. Passed on into a Buffer slot, out stays the NULL Buffer, which a callee with the same entry test reads as undefined. The parameter keeps its static type, so callers and adapters are unchanged. A copy that has no such lowering throws SC2020 when it runs. Conditions on the error parameter fold in the error-path copy, and in the success copy for a literal that only the adapter calls. Inside an error-path copy, a nested callback lowers once with its result read as any, so nested callbacks grow linearly in code size. --- .../compiler/src/backend/llvm/lib-dispatch.ts | 8 + .../frontend/lowering/builtins/arguments.ts | 45 ++- .../src/frontend/lowering/lower-functions.ts | 377 ++++++++++++++---- .../compiler/src/frontend/lowering/lowerer.ts | 42 ++ .../src/frontend/lowering/static-typeof.ts | 42 +- .../compiler/src/ir/builtin-signatures.ts | 8 + .../6920-zlib-callback-error-result.ts.json | 6 + .../corpus/6920-zlib-callback-error-result.ts | 195 +++++++++ .../6920-zlib-callback-error-result.ts.json | 1 + 9 files changed, 634 insertions(+), 90 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6920-zlib-callback-error-result.ts.json create mode 100644 tests/corpus/6920-zlib-callback-error-result.ts create mode 100644 tests/harness/corpus-costs/6920-zlib-callback-error-result.ts.json diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 924027ee29..526d4b7e0c 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -758,6 +758,14 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const inline = emitInlineAtomicsOp(host, e); if (inline) return inline; } + if (e.fn === "bytes.isAbsent") { + // A borrowed read: retaining an absent Buffer would dereference NULL. + const value = host.emitReadReceiver(e.args[0]!); + const t = host.B.tmp(); + host.B.line(`${t} = icmp eq ptr ${value.name}, null`); + return { name: t, type: e.type }; + } + if (e.fn === "bytes.absent") return host.own({ name: "null", type: e.type }); const prefix = e.fn.slice(0, e.fn.indexOf(".")) as LibCallPrefix; // events.once builds typed listener adapters beside the emitter calls. if (e.fn === "events.once" || e.fn === "events.onceSignal") diff --git a/packages/compiler/src/frontend/lowering/builtins/arguments.ts b/packages/compiler/src/frontend/lowering/builtins/arguments.ts index 5e21f6cb3f..d3bf467749 100644 --- a/packages/compiler/src/frontend/lowering/builtins/arguments.ts +++ b/packages/compiler/src/frontend/lowering/builtins/arguments.ts @@ -238,6 +238,25 @@ export function errorFirstBytesCallback( ); } +/** Whether `type` is an error-first callback's error parameter type: + * Error | null, optionally with undefined, or dyn. */ +export function isErrorFirstErrorType(lowerer: Lowerer, type: IrType): boolean { + if (type.kind === "dyn") return true; + if (type.kind !== "union") return false; + const def = lowerer.unions.get(type.unionId); + return ( + !!def && + def.arms.some((arm) => arm.kind === "nullT") && + def.arms.some((arm) => arm.kind === "object" && arm.className === "%Error") && + def.arms.every( + (arm) => + arm.kind === "nullT" || + arm.kind === "undefinedT" || + (arm.kind === "object" && arm.className === "%Error"), + ) + ); +} + /** An error-first callback (error, value) whose error parameter is * Error | null or dyn and whose value parameter is dyn or accepted by * `acceptValue`; `values` names the accepted values in the refusal. */ @@ -248,6 +267,12 @@ export function errorFirstCallback( acceptValue: (value: IrType) => boolean, values: string, ): IrExpr { + // A literal here is called only by the callback adapter, which passes + // null as the error whenever it passes a result (lowerUndefinedResultBody). + let literal: ts.Expression = node; + while (ts.isParenthesizedExpression(literal)) literal = literal.expression; + if (ts.isArrowFunction(literal) || ts.isFunctionExpression(literal)) + lowerer.errorFirstLiterals.add(literal); let callback = lowerer.lowerExpr(node); if (callback.type.kind === "dyn") { callback = { @@ -261,24 +286,8 @@ export function errorFirstCallback( lowerer.unsupported("SC1090", node, `${api} callbacks must accept at most (error, buffer)`); } const error = callback.type.params[0]; - if (error !== undefined && error.kind !== "dyn") { - if (error.kind !== "union") { - lowerer.unsupported("SC1090", node, `${api} callback error parameters must be Error | null`); - } - const def = lowerer.unions.get(error.unionId); - const valid = - !!def && - def.arms.some((arm) => arm.kind === "nullT") && - def.arms.some((arm) => arm.kind === "object" && arm.className === "%Error") && - def.arms.every( - (arm) => - arm.kind === "nullT" || - arm.kind === "undefinedT" || - (arm.kind === "object" && arm.className === "%Error"), - ); - if (!valid) - lowerer.unsupported("SC1090", node, `${api} callback error parameters must be Error | null`); - } + if (error !== undefined && !isErrorFirstErrorType(lowerer, error)) + lowerer.unsupported("SC1090", node, `${api} callback error parameters must be Error | null`); const value = callback.type.params[1]; if (value !== undefined && value.kind !== "dyn" && !acceptValue(value)) { lowerer.unsupported( diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 0ddfa0ac44..9254e62134 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -1,14 +1,19 @@ import { declareTypedArguments } from "./typed-arguments.js"; -import { varRef } from "../../ir/build.js"; +import { dynUndefinedExpr, varRef } from "../../ir/build.js"; +import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; +import { bindingNeverReassigned } from "./binding-analysis.js"; +import { isErrorFirstErrorType } from "./builtins/arguments.js"; import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; import { isRebindableFunctionSymbol } from "./rebindable-functions.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import type { Lowerer } from "./lowerer.js"; import { + BOOL, DYN, type IrExpr, type IrFunction, + type IrLocal, type IrParam, type IrStmt, type SrcLoc, @@ -253,6 +258,14 @@ export function lowerLambda( shapes, funcType, ); + // An error-first callback that reads its Buffer result: undefined on the + // error path, as in Node. + const undefinedResult = undefinedResultParam( + lowerer, + node, + shapes, + lowerer.errorFirstLiterals.has(node), + ); const nameIdent = !ts.isArrowFunction(node) && node.name && ts.isIdentifier(node.name) ? node.name : null; const baseName = nameIdent ? nameIdent.text : ""; @@ -340,78 +353,84 @@ export function lowerLambda( !shapes.some((s) => s.mode === "dynRest"), ); - let body: IrStmt[]; - if (ts.isBlock(node.body!)) { - body = lowerer.lowerStmts(node.body!.statements.nodes); - } else { - // Bare-expression arrow body: `x => e` is `x => { return e; }` - // (or an expression statement when the signature returns void — or - // when a union-returning signature wraps a void expression, whose - // value is the implicit undefined arm appended below). - const bodyExpr = node.body as ts.Expression; - if (bodyReturn.kind === "void") { - // `() => undefined` — the return type maps to void (standalone - // undefined IS void in the type mapping) and the body value is a - // bare unit literal: a pure no-op, dropped rather than tripping - // the validator's bare-unitLit rule (typeCheckReturnExpression). - // A `void e` body rides the statement lowering (the value is - // discarded here, so the operand evaluates for effect alone — - // `(name) => void doThing(name)`, the fire-and-forget arrow). A - // conditional body does the same so its void arms become lazy - // statement branches instead of a forbidden void-valued ternary. - let stripped: ts.Expression = bodyExpr; - while (ts.isParenthesizedExpression(stripped)) stripped = stripped.expression; - if (ts.isVoidExpression(stripped) || ts.isConditionalExpression(stripped)) { - body = [lowerer.lowerExprStatement(stripped)]; + const lowerBody = (): IrStmt[] => { + let body: IrStmt[]; + if (ts.isBlock(node.body!)) { + body = lowerer.lowerStmts(node.body!.statements.nodes); + } else { + // Bare-expression arrow body: `x => e` is `x => { return e; }` + // (or an expression statement when the signature returns void — or + // when a union-returning signature wraps a void expression, whose + // value is the implicit undefined arm appended below). + const bodyExpr = node.body as ts.Expression; + if (bodyReturn.kind === "void") { + // `() => undefined` — the return type maps to void (standalone + // undefined IS void in the type mapping) and the body value is a + // bare unit literal: a pure no-op, dropped rather than tripping + // the validator's bare-unitLit rule (typeCheckReturnExpression). + // A `void e` body rides the statement lowering (the value is + // discarded here, so the operand evaluates for effect alone — + // `(name) => void doThing(name)`, the fire-and-forget arrow). A + // conditional body does the same so its void arms become lazy + // statement branches instead of a forbidden void-valued ternary. + let stripped: ts.Expression = bodyExpr; + while (ts.isParenthesizedExpression(stripped)) stripped = stripped.expression; + if (ts.isVoidExpression(stripped) || ts.isConditionalExpression(stripped)) { + body = [lowerer.lowerExprStatement(stripped)]; + } else { + const value = lowerer.lowerExpr(bodyExpr); + // `async () => p` with a void result still resolves the + // function's promise with p (its settlement, rejections included); + // a maybe-promise value adopts only its promise arm. + const flattened = isAsync ? lowerer.maybePromiseReturn(bodyExpr, value, VOID) : null; + body = flattened + ? [flattened] + : value.kind === "unitLit" + ? [] + : isAsync && value.type.kind === "promise" + ? [lowerer.adoptingReturn(value, VOID, (e) => e)] + : [{ kind: "exprStmt", expr: value, loc: locOf(node.body!) }]; + } } else { - const value = lowerer.lowerExpr(bodyExpr); - // `async () => p` with a void result still resolves the - // function's promise with p (its settlement, rejections included); - // a maybe-promise value adopts only its promise arm. - const flattened = isAsync ? lowerer.maybePromiseReturn(bodyExpr, value, VOID) : null; + // Concise and block bodies use the same destination layout for + // fresh literals. Async bodies must inspect promises first. + let value = + isAsync || fnCtx.inferReturn + ? lowerer.lowerExpr(bodyExpr) + : lowerer.lowerExprExpecting(bodyExpr, bodyReturn); + // An async concise body whose value is itself a promise + // (`async () => p`): the async machinery RESOLVES the returned + // thenable into the function's own promise — lowerReturnValue's + // await-through, applied to the implicit return. + if (isAsync && value.type.kind === "promise" && bodyReturn.kind !== "promise") { + value = lowerer.returnedPromiseAwait(value); + } + const flattened = + isAsync && !fnCtx.inferReturn + ? lowerer.maybePromiseReturn(bodyExpr, value, bodyReturn) + : null; body = flattened ? [flattened] - : value.kind === "unitLit" - ? [] - : isAsync && value.type.kind === "promise" - ? [lowerer.adoptingReturn(value, VOID, (e) => e)] - : [{ kind: "exprStmt", expr: value, loc: locOf(node.body!) }]; + : value.type.kind === "void" && lowerer.wrappedUndefined(bodyReturn, locOf(node.body!)) + ? [{ kind: "exprStmt", expr: value, loc: locOf(node.body!) }] + : [ + { + kind: "return", + value: fnCtx.inferReturn + ? value + : lowerer.coerceInto(bodyExpr, value, bodyReturn), + loc: locOf(node.body!), + }, + ]; + if (fnCtx.inferReturn && body[0]?.kind === "return") + fnCtx.inferReturn.entries.push({ stmt: body[0], node: bodyExpr }); } - } else { - // Concise and block bodies use the same destination layout for - // fresh literals. Async bodies must inspect promises first. - let value = - isAsync || fnCtx.inferReturn - ? lowerer.lowerExpr(bodyExpr) - : lowerer.lowerExprExpecting(bodyExpr, bodyReturn); - // An async concise body whose value is itself a promise - // (`async () => p`): the async machinery RESOLVES the returned - // thenable into the function's own promise — lowerReturnValue's - // await-through, applied to the implicit return. - if (isAsync && value.type.kind === "promise" && bodyReturn.kind !== "promise") { - value = lowerer.returnedPromiseAwait(value); - } - const flattened = - isAsync && !fnCtx.inferReturn - ? lowerer.maybePromiseReturn(bodyExpr, value, bodyReturn) - : null; - body = flattened - ? [flattened] - : value.type.kind === "void" && lowerer.wrappedUndefined(bodyReturn, locOf(node.body!)) - ? [{ kind: "exprStmt", expr: value, loc: locOf(node.body!) }] - : [ - { - kind: "return", - value: fnCtx.inferReturn - ? value - : lowerer.coerceInto(bodyExpr, value, bodyReturn), - loc: locOf(node.body!), - }, - ]; - if (fnCtx.inferReturn && body[0]?.kind === "return") - fnCtx.inferReturn.entries.push({ stmt: body[0], node: bodyExpr }); } - } + return body; + }; + let body = undefinedResult + ? lowerUndefinedResultBody(lowerer, node, undefinedResult, lowerBody) + : lowerBody(); if (fnCtx.inferReturn) { bodyReturn = resolveInferredReturn( lowerer, @@ -537,7 +556,14 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I "function declarations whose block body the frontend cannot locate", ); } - const body = [...prologue, ...lowerer.lowerStmts(bodyBlock.statements.nodes)]; + const undefinedResult = undefinedResultParam(lowerer, decl, sig.params, false); + const lowerBody = (): IrStmt[] => lowerer.lowerStmts(bodyBlock.statements.nodes); + const body = [ + ...prologue, + ...(undefinedResult + ? lowerUndefinedResultBody(lowerer, decl, undefinedResult, lowerBody) + : lowerBody()), + ]; appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(decl)); const fn: IrFunction = { name: sig.name, @@ -594,3 +620,216 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I lowerer.fnStack.pop(); } } + +/** The parameters of an error-first callback whose Buffer result its body + * reads: `(err, out)` with err typed Error | null (or any) and out a + * Buffer. The callback adapters of zlib and crypto pass a NULL Buffer + * after an error, where Node passes undefined. `error` is the error + * parameter's symbol when nothing writes it; `onlyAdapterCalls` marks a + * literal passed straight to such a function, which nothing else calls. */ +interface UndefinedResultParam { + symbol: ts.Symbol; + name: ts.Identifier; + error: ts.Symbol | null; + onlyAdapterCalls: boolean; +} + +function undefinedResultParam( + lowerer: Lowerer, + node: ts.SignatureDeclaration, + shapes: readonly ParamShape[], + onlyAdapterCalls: boolean, +): UndefinedResultParam | null { + const params = node.parameters.nodes.filter((p) => !isThisParameter(p)); + if (params.length !== 2 || shapes.length !== 2 || node.asteriskToken !== undefined) return null; + const [errorDecl, decl] = params as [ts.ParameterDeclaration, ts.ParameterDeclaration]; + const [errorShape, shape] = shapes as [ParamShape, ParamShape]; + if ( + !ts.isIdentifier(decl.name) || + decl.initializer !== undefined || + decl.questionToken !== undefined || + decl.dotDotDotToken !== undefined || + shape.mode !== "required" || + shape.type.kind !== "bytes" || + shape.type.elem !== "u8" || + errorShape.mode !== "required" || + !isErrorFirstErrorType(lowerer, errorShape.type) + ) + return null; + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + if ( + symbol === undefined || + node.body === undefined || + !readsSymbol(lowerer, node.body, symbol, decl.name) + ) + return null; + const errorSymbol = ts.isIdentifier(errorDecl.name) + ? lowerer.checker.getSymbolAtLocation(errorDecl.name) + : undefined; + const error = + errorSymbol !== undefined && + errorDecl.initializer === undefined && + bindingNeverReassigned(lowerer, errorSymbol, errorDecl) + ? errorSymbol + : null; + return { symbol, name: decl.name, error, onlyAdapterCalls }; +} + +/** Whether `body` reads `symbol` (other than at its declaration `name`). */ +function readsSymbol( + lowerer: Lowerer, + body: ts.Node, + symbol: ts.Symbol, + name: ts.Identifier, +): boolean { + let read = false; + const visit = (n: ts.Node): void => { + if (read) return; + if (ts.isShorthandPropertyAssignment(n)) { + if (lowerer.checker.getShorthandAssignmentValueSymbol(n) === symbol) read = true; + } else if (ts.isIdentifier(n)) { + if (n !== name && lowerer.checker.getSymbolAtLocation(n) === symbol) read = true; + } else { + ts.forEachChild(n, visit); + } + }; + visit(body); + return read; +} + +/** The body of an error-first callback whose Buffer result may be absent. + * With a result, the body lowers as written. Without one (the adapter's + * NULL Buffer after an error), the body lowers again with the result read + * as `any` holding undefined and the error parameter holding an Error. + * Conditions on the error parameter fold in that copy (staticCondition), + * and in the first copy too, as null, for a literal only the adapter + * calls: a body that returns after `if (err)` then keeps the rest of its + * statements in the first copy only. + * + * Inside an error-path copy, a nested callback lowers once with the result + * read as `any`, so nested callbacks do not double at every level. An + * error-path copy with no lowering throws a coded error when it runs. */ +function lowerUndefinedResultBody( + lowerer: Lowerer, + node: ts.SignatureDeclaration, + result: UndefinedResultParam, + lowerBody: () => IrStmt[], +): IrStmt[] { + const loc = locOf(node); + const param = lowerer.bindingIn(lowerer.ctx, result.symbol); + if (param === null) throw new InternalCompilerError("lowerer bug: callback result unbound"); + const absent: IrExpr = { + kind: "libCall", + fn: "bytes.isAbsent", + args: [varRef(param.id, param.type, loc)], + type: BOOL, + loc, + }; + const fence: IrStmt = { + kind: "runtimeFence", + code: "SC2020", + message: `the callback's '${result.name.text}' is undefined after an error, and this callback has no lowering that reads it as undefined`, + loc, + }; + const withKnownError = (error: boolean | null, lower: () => IrStmt[]): IrStmt[] => { + if (result.error === null || error === null) return lower(); + lowerer.knownErrors.set(result.error, error); + try { + return lower(); + } finally { + lowerer.knownErrors.delete(result.error); + } + }; + /** The body with the result read as `any` from `slot`, or null when it + * has no such lowering or converts the result into a static slot. */ + const lowerDynamic = (slot: IrLocal, knownError: boolean | null): IrStmt[] | null => { + const diags = lowerer.diags.length; + const stats = lowerer.suppressStats; + lowerer.scopes.push(new Map([[result.symbol, slot]])); + lowerer.anySymbols.add(result.symbol); + lowerer.suppressStats = true; + lowerer.errorPathCopies++; + try { + // Passed on into a Buffer slot, the result stays the absent Buffer + // the adapter passed: a callee with this entry test reads it as + // undefined. + const body = transformStmtList(withKnownError(knownError, lowerBody), { + expr: (e) => + e.kind === "dynCheck" && + e.value.kind === "varRef" && + e.value.localId === slot.id && + e.type.kind === "bytes" && + e.type.elem === "u8" + ? { kind: "libCall", fn: "bytes.absent", args: [], type: e.type, loc: e.loc } + : e, + stmt: (stmt) => stmt, + }); + if (lowerer.diags.length > diags || !readsSlotDynamically(body, slot.id)) + throw new PoisonError(); + return body; + } catch (error) { + if (error instanceof InternalCompilerError) throw error; + lowerer.diags.length = diags; + return null; + } finally { + lowerer.errorPathCopies--; + lowerer.anySymbols.delete(result.symbol); + lowerer.scopes.pop(); + lowerer.suppressStats = stats; + } + }; + const slot = lowerer.declareHiddenLocal("%result", DYN); + const undefinedResult: IrStmt = { + kind: "varDecl", + localId: slot.id, + init: dynUndefinedExpr(loc), + loc, + }; + if (lowerer.errorPathCopies > 0) { + // One copy for both paths, with the result boxed when present. + const dynamic = lowerDynamic(slot, null); + if (dynamic !== null) + return [ + { + kind: "varDecl", + localId: slot.id, + init: { + kind: "ternary", + cond: absent, + then: dynUndefinedExpr(loc), + else_: lowerer.coerceInto(result.name, varRef(param.id, param.type, loc), DYN), + type: DYN, + loc, + }, + loc, + }, + ...dynamic, + ]; + } + const withResult = withKnownError(result.onlyAdapterCalls ? false : null, lowerBody); + const withoutResult = lowerDynamic(slot, true); + return [ + { + kind: "if", + cond: absent, + then: withoutResult === null ? [fence] : [undefinedResult, ...withoutResult], + else_: withResult, + loc, + }, + ]; +} + +/** Whether the error-path copy reads the checked-dynamic result slot only as + * a checked-dynamic value, never converted into a static slot (where Node + * would hold undefined). */ +function readsSlotDynamically(body: IrStmt[], slotId: string): boolean { + const isSlot = (e: IrExpr): boolean => e.kind === "varRef" && e.localId === slotId; + return everyStmtList(body, { + expr: (e) => { + if (isSlot(e)) return e.type.kind === "dyn"; + if (e.kind === "dynCheck" && isSlot(e.value)) return false; + return true; + }, + stmt: () => true, + }); +} diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index d81ac546e7..40b0bc8749 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2655,6 +2655,19 @@ export class Lowerer { /** True while re-lowering a base function's 2nd+ instance: the same source * statements were already counted for the first instance. */ suppressStats = false; + /** Callback literals passed straight to an error-first zlib or crypto + * function: only that function's callback adapter calls them + * (lowerUndefinedResultBody). */ + readonly errorFirstLiterals = new Set(); + /** Parameters read as `any` in the error-path copy of an error-first + * callback. */ + readonly anySymbols = new Set(); + /** Error parameters of an error-first callback with a known value in + * the copy being lowered: true while it holds an Error, false while it + * holds null (staticCondition folds conditions on them). */ + readonly knownErrors = new Map(); + /** The number of error-path callback copies being lowered. */ + errorPathCopies = 0; /** Comparisons interned by operand representations and equality semantics. */ readonly equalityHelpers = new Map(); /** Synthetic array-HOF loop functions (map/filter/forEach desugar), @@ -8382,7 +8395,36 @@ export class Lowerer { return fenceStaticReadableStreamMember(this, access, use); } + /** Whether `node` reads an anySymbols parameter, or a member, element or + * call result of one: the error-path copy of a callback reads those as + * `any`. */ + private readsAnySymbol(node: ts.Node): boolean { + let base: ts.Node = node; + for (;;) { + if ( + ts.isPropertyAccessExpression(base) || + ts.isElementAccessExpression(base) || + ts.isNonNullExpression(base) || + ts.isParenthesizedExpression(base) + ) { + base = base.expression; + } else if (ts.isCallExpression(base) && ts.isPropertyAccessExpression(base.expression)) { + base = base.expression.expression; + } else { + break; + } + } + if (!ts.isIdentifier(base)) return false; + const parent = base.parent; + const symbol = + parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === base + ? this.checker.getShorthandAssignmentValueSymbol(parent) + : this.checker.getSymbolAtLocation(base); + return symbol !== undefined && this.anySymbols.has(symbol); + } + typeOf(node: ts.Node): ts.Type { + if (this.anySymbols.size > 0 && this.readsAnySymbol(node)) return this.checker.getAnyType(); if (ts.isPropertyAccessExpression(node) && isJsSourceFile(ts.getSourceFileOfNode(node))) { const receiver = node.expression.kind === ts.SyntaxKind.ThisKeyword && this.fnStack.length > 0 diff --git a/packages/compiler/src/frontend/lowering/static-typeof.ts b/packages/compiler/src/frontend/lowering/static-typeof.ts index 54f2fdfa04..852cc31175 100644 --- a/packages/compiler/src/frontend/lowering/static-typeof.ts +++ b/packages/compiler/src/frontend/lowering/static-typeof.ts @@ -63,9 +63,44 @@ export function staticTypeofComparison(lowerer: Lowerer, e: ts.BinaryExpression) } } +/** Whether a callback's error parameter holds an Error (true) or null + * (false) in the copy of the callback body being lowered + * (lowerUndefinedResultBody), or null for other expressions. */ +function knownError(lowerer: Lowerer, e: ts.Expression): boolean | null { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (lowerer.knownErrors.size === 0 || !ts.isIdentifier(e)) return null; + const symbol = lowerer.checker.getSymbolAtLocation(e); + return symbol === undefined ? null : (lowerer.knownErrors.get(symbol) ?? null); +} + +/** A comparison of a known error parameter against `null` + * (`err !== null`, `err == null`), folded. Null for other shapes. */ +function knownErrorComparison(lowerer: Lowerer, e: ts.BinaryExpression): boolean | null { + const isNull = (x: ts.Expression): boolean => { + while (ts.isParenthesizedExpression(x)) x = x.expression; + return x.kind === ts.SyntaxKind.NullKeyword; + }; + const error = isNull(e.right) + ? knownError(lowerer, e.left) + : isNull(e.left) + ? knownError(lowerer, e.right) + : null; + if (error === null) return null; + switch (e.operatorToken.kind) { + case ts.SyntaxKind.EqualsEqualsEqualsToken: + case ts.SyntaxKind.EqualsEqualsToken: + return !error; + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsToken: + return error; + default: + return null; + } +} + /** The value of a condition built from statically answered typeof - * comparisons, `!`, `&&` and `||` (the global-detection chains), or null - * when any operand needs evaluation. */ + * comparisons, known callback error parameters, `!`, `&&` and `||` (the + * global-detection chains), or null when any operand needs evaluation. */ export function staticCondition(lowerer: Lowerer, expr: ts.Expression): boolean | null { let e = expr; while (ts.isParenthesizedExpression(e)) e = e.expression; @@ -73,6 +108,7 @@ export function staticCondition(lowerer: Lowerer, expr: ts.Expression): boolean const inner = staticCondition(lowerer, e.operand); return inner === null ? null : !inner; } + if (ts.isIdentifier(e)) return knownError(lowerer, e); if (!ts.isBinaryExpression(e)) return null; const op = e.operatorToken.kind; if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) { @@ -82,7 +118,7 @@ export function staticCondition(lowerer: Lowerer, expr: ts.Expression): boolean if (left !== isAnd) return left; return staticCondition(lowerer, e.right); } - return staticTypeofComparison(lowerer, e); + return staticTypeofComparison(lowerer, e) ?? knownErrorComparison(lowerer, e); } /** The expression a value reduces to once statically decided conditions diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 9f383c695e..28da4ff612 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4616,6 +4616,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * lone surrogate as U+FFFD). Borrowed arg; owned (+1) string; never throws. */ "text.decode": { argTypes: [BYTES_U8], result: STRING }, "bytes.bufferSource": { argTypes: [DYN], result: BYTES_U8 }, + /** Whether an error-first callback's Buffer result is absent: the zlib + * and crypto callback adapters pass NULL after an error. Borrowed arg; + * never throws. */ + "bytes.isAbsent": { argTypes: [BYTES_U8], result: BOOL }, + /** The absent Buffer result itself (NULL), where an error-path callback + * body passes its undefined result on into a Buffer slot. Owned (+1); + * never throws. */ + "bytes.absent": { argTypes: [], result: BYTES_U8 }, /** The Buffer brand of a Uint8Array source onto a fresh Uint8Array * result (map and filter of a Buffer return a Buffer). */ "bytes.copyBrand": { argTypes: [BYTES_U8, BYTES_U8], result: VOID }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6920-zlib-callback-error-result.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6920-zlib-callback-error-result.ts.json new file mode 100644 index 0000000000..05e1384411 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6920-zlib-callback-error-result.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6920-zlib-callback-error-result.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6920-zlib-callback-error-result.ts b/tests/corpus/6920-zlib-callback-error-result.ts new file mode 100644 index 0000000000..50998d37f6 --- /dev/null +++ b/tests/corpus/6920-zlib-callback-error-result.ts @@ -0,0 +1,195 @@ +// zlib callbacks receive undefined as the result when they fail, as in +// Node: the result reads as undefined in logs, templates, typeof and +// comparisons, member reads throw Node's TypeError, and the same callbacks +// read the Buffer as before when the operation succeeds. This holds for +// callback literals and for function values. +import * as zlib from "node:zlib"; + +const good = zlib.deflateSync("hello callbacks"); +const bad = Buffer.from("not deflate data"); + +function report(label: string, run: () => void): void { + try { + run(); + } catch (e) { + console.log(label, "threw", (e as Error).name, (e as Error).message); + } +} + +function check(label: string, input: Buffer, next: () => void): void { + zlib.inflate(input, (err, out) => { + console.log(label, "error", err?.code ?? null); + console.log(label, "value", out); + console.log(label, "template", `${out}`, String(out), out + ""); + console.log(label, "typeof", typeof out, out === undefined, out !== undefined); + console.log(label, "optional", out?.length, out?.toString()); + console.log(label, "isBuffer", Buffer.isBuffer(out)); + console.log(label, out ? "truthy" : "falsy"); + report(label + " length", () => console.log(label, "length", out.length)); + report(label + " toString", () => console.log(label, "text", out.toString())); + report(label + " inflateSync", () => console.log(label, "again", zlib.inflateSync(out).length)); + setTimeout(() => { + console.log(label, "later", typeof out, out?.byteLength); + next(); + }); + }); +} + +function guarded(next: () => void): void { + // A callback that checks the error first reads the Buffer only on success. + zlib.gunzip(bad, (err, out) => { + if (err) { + console.log("guarded error", err.code, err.errno); + zlib.gzip("guarded text", (err2, packed) => { + if (err2) throw err2; + zlib.gunzip(packed, (err3, plain) => { + console.log("guarded round trip", err3, plain.toString()); + next(); + }); + }); + return; + } + console.log("guarded value", out.toString()); + }); +} + +function expressions(): void { + // Expression bodies. + zlib.unzip(bad, (err, out) => + zlib.deflateRaw("raw", (err2, raw) => console.log("expression", err?.code, out, err2, raw.length > 0)), + ); +} + +function everyFunction(next: () => void): void { + // Each callback function, with and without options. maxOutputLength makes + // the compressors fail; a non-matching format makes the decompressors fail. + const text = "every function ".repeat(20); + const opts = { maxOutputLength: 8 }; + zlib.deflate(text, opts, (err, out) => { + console.log("deflate", err?.code, typeof out, out?.length); + zlib.deflateRaw(text, opts, (err, out) => { + console.log("deflateRaw", err?.code, typeof out, out?.length); + zlib.gzip(text, opts, (err, out) => { + console.log("gzip", err?.code, typeof out, out?.length); + zlib.inflateRaw(bad, (err, out) => { + console.log("inflateRaw", err?.code, typeof out, out?.length); + zlib.inflate(zlib.deflateSync(text), opts, (err, out) => { + console.log("inflate options", err?.code, typeof out, out?.length); + zlib.gunzip(zlib.gzipSync(text), opts, (err, out) => { + console.log("gunzip options", err?.code, typeof out, out?.length); + zlib.unzip(bad, {}, (err, out) => { + console.log("unzip options", err?.code, typeof out, out?.length); + zlib.inflateRaw(zlib.deflateRawSync(text), {}, (err, out) => { + console.log("inflateRaw options", err, out.toString() === text); + zlib.deflate(text, {}, (err, out) => { + console.log("deflate options", err, zlib.inflateSync(out).toString() === text); + next(); + }); + }); + }); + }); + }); + }); + }); + }); + }); +} + +function folded(input: Buffer, next: () => void): void { + // Conditions on the error parameter. + zlib.inflateRaw(input, (err, out) => { + if (err !== null) console.log("folded error", err.code, typeof out); + else console.log("folded value", out.length); + if (!err) console.log("folded ok", out.toString()); + console.log("folded both", err == null ? out.length : typeof out, err && err.code); + if (err) return; + console.log("folded after", out.byteLength); + }); + setTimeout(next, 10); +} + +// Function values: declarations, const arrows and forwarded parameters +// receive undefined too, and a direct call still passes its arguments. +const steps: (() => void)[] = []; +function step(): void { + const run = steps.shift(); + if (run) run(); +} +function named(err: Error | null, out: Buffer): void { + if (err) console.log("named error", (err as NodeJS.ErrnoException).code, typeof out, out === undefined); + else console.log("named value", out.toString()); + console.log("named both", err !== null, typeof out, `${out}`, out?.length); + step(); +} +const arrow = (err: Error | null, out: Buffer): void => { + console.log("arrow", err?.message ?? null, typeof out, String(out)); + report("arrow length", () => console.log("arrow length", out.length)); + step(); +}; +function captures(err: Error | null, out: Buffer): void { + setTimeout(() => { + console.log("captured", err !== null, typeof out, out?.length); + step(); + }); +} +function forward(input: Buffer, cb: (err: Error | null, out: Buffer) => void): void { + zlib.inflate(input, cb); +} +function forwardGzip(text: string, cb: (err: Error | null, out: Buffer) => void): void { + zlib.gzip(text, { maxOutputLength: 4 }, cb); +} + +function forwardLiteral(input: Buffer, cb: (err: Error | null, out: Buffer) => void): void { + // The literal passes its result on to a typed callback. + zlib.inflate(input, (err, out) => cb(err, out)); +} +function inflated(input: Buffer): Promise { + return new Promise((resolve, reject) => + zlib.inflate(input, (err, out) => (err ? reject(err) : resolve(out))), + ); +} + +function functionValues(next: () => void): void { + steps.push( + () => zlib.inflate(good, named), + () => zlib.inflate(bad, named), + () => zlib.inflate(good, arrow), + () => zlib.inflate(bad, arrow), + () => forward(bad, named), + () => forwardLiteral(bad, named), + () => forwardLiteral(good, arrow), + () => + inflated(bad).then( + (out) => console.log("promise value", out.length), + (e) => { + console.log("promise rejected", (e as NodeJS.ErrnoException).code); + step(); + }, + ), + () => + void inflated(good).then((out) => { + console.log("promise value", out.toString()); + step(); + }), + () => forwardGzip("text that does not fit", arrow), + () => zlib.inflate(bad, captures), + () => zlib.inflate(good, captures), + () => { + named(new Error("direct"), Buffer.from("both")); + }, + next, + ); + step(); +} + +check("good", good, () => + check("bad", bad, () => + guarded(() => + everyFunction(() => + folded(zlib.deflateRawSync("folded text"), () => + folded(bad, () => functionValues(expressions)), + ), + ), + ), + ), +); diff --git a/tests/harness/corpus-costs/6920-zlib-callback-error-result.ts.json b/tests/harness/corpus-costs/6920-zlib-callback-error-result.ts.json new file mode 100644 index 0000000000..1241a31c06 --- /dev/null +++ b/tests/harness/corpus-costs/6920-zlib-callback-error-result.ts.json @@ -0,0 +1 @@ +{ "plain": 5517, "san": 7607 } From a7e84cd48c34bfee5ab3c5e09460e0d85d5b069d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 13:56:07 -0500 Subject: [PATCH 006/395] Support SNICallback in tls.createServer tls.createServer({ cert, key, SNICallback }) hands each ClientHello's servername to the callback and serves the SecureContext it answers, synchronously or later, or the default cert and key when it answers none. An error answer closes that connection. This uses the SNI handling that http2.createSecureServer already has. A callback expression with side effects, or SNICallback combined with ALPNProtocols, minVersion, maxVersion or the other options the runtime walk reads, fails with SC2020. A tls.connect or https.request client whose peer closes or resets the connection before the handshake completes now fails with Node's ECONNRESET "Client network socket disconnected before secure TLS connection was established" instead of closing without an error. Refs #1438 Co-authored-by: saboursepideh <25549001+saboursepideh@users.noreply.github.com> --- .../compiler/src/backend/llvm/lib-http2.ts | 34 ++-- packages/compiler/src/backend/llvm/lib-tls.ts | 22 +++ .../src/frontend/lowering/lower-server.ts | 157 ++++++++++++------ .../compiler/src/ir/builtin-signatures.ts | 5 + .../corpus/6933-tls-sni-callback.ts.json | 6 + .../6934-tls-handshake-disconnect.ts.json | 6 + .../tls-sni-callback-fences.ts.json | 6 + packages/runtime/src/scr_error.c | 6 +- packages/runtime/src/scr_tls.c | 35 +++- packages/runtime/src/scr_tls.h | 3 + tests/corpus/6933-tls-sni-callback.ts | 65 ++++++++ tests/corpus/6934-tls-handshake-disconnect.ts | 27 +++ tests/diagnostics/tls-sni-callback-fences.ts | 15 ++ .../tls-sni-callback-fences.ts.txt | 17 ++ 14 files changed, 330 insertions(+), 74 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6933-tls-sni-callback.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6934-tls-handshake-disconnect.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/tls-sni-callback-fences.ts.json create mode 100644 tests/corpus/6933-tls-sni-callback.ts create mode 100644 tests/corpus/6934-tls-handshake-disconnect.ts create mode 100644 tests/diagnostics/tls-sni-callback-fences.ts create mode 100644 tests/harness/__snapshots__/tls-sni-callback-fences.ts.txt diff --git a/packages/compiler/src/backend/llvm/lib-http2.ts b/packages/compiler/src/backend/llvm/lib-http2.ts index 0254db5557..791987f86f 100644 --- a/packages/compiler/src/backend/llvm/lib-http2.ts +++ b/packages/compiler/src/backend/llvm/lib-http2.ts @@ -3,6 +3,7 @@ import type { IrType } from "../../ir/ir.js"; import { typeKey } from "../../ir/ir.js"; import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js"; import { + type AbiArgument, abiValue, callbackAdapter, callbackType, @@ -84,6 +85,23 @@ function sniAnswer(host: LlvmEmitterContext, type: IrType): string { return symbol; } +/** The (closure, answer thunk) pair of an SNICallback argument: a function, + * or a function | undefined union whose undefined arm passes no callback. */ +export function sniCallbackArgs(host: LlvmEmitterContext, cb: LlValue): AbiArgument[] { + let type = cb.type, + value = cb.name; + if (type.kind === "union") { + const arms = host.unionsById.get(type.unionId)?.arms ?? []; + const tag = arms.findIndex((arm) => arm.kind === "func"); + if (tag < 0) throw new InternalCompilerError("SNI callback union lacks its function arm"); + type = arms[tag]!; + value = host.unwrapNullableClosure(value, tag); + } else host.toHeap(cb); + if (type.kind !== "func" || type.params[1] === undefined) + throw new InternalCompilerError("SNI callback lacks its answer function"); + return [ptr(value), ptr("@" + sniAnswer(host, type.params[1]))]; +} + export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const args = e.args.map((arg) => host.emitExpr(arg)), a = args.map((arg) => abiValue(host, arg)); @@ -160,21 +178,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa let handler = [ptr(), ptr()], sni = [ptr(), ptr()]; if (e.fn === "http2.createSecureServerReq") handler = requestHandler(host, args[2]!); - if (e.fn === "http2.createSecureServerSni") { - const cb = args[2]!; - let type = cb.type, - value = cb.name; - if (type.kind === "union") { - const arms = host.unionsById.get(type.unionId)?.arms ?? []; - const tag = arms.findIndex((arm) => arm.kind === "func"); - if (tag < 0) throw new InternalCompilerError("SNI callback union lacks its function arm"); - type = arms[tag]!; - value = host.unwrapNullableClosure(value, tag); - } else host.toHeap(cb); - if (type.kind !== "func" || type.params[1] === undefined) - throw new InternalCompilerError("SNI callback lacks its answer function"); - sni = [ptr(value), ptr("@" + sniAnswer(host, type.params[1]))]; - } + if (e.fn === "http2.createSecureServerSni") sni = sniCallbackArgs(host, args[2]!); return finishRuntimeCall(host, e, "scr_http2_create_secure_server_allow_http1", [ ...rawBytes(host, args[0]!), ...rawBytes(host, args[1]!), diff --git a/packages/compiler/src/backend/llvm/lib-tls.ts b/packages/compiler/src/backend/llvm/lib-tls.ts index 18fc006d47..c5c77d3a6e 100644 --- a/packages/compiler/src/backend/llvm/lib-tls.ts +++ b/packages/compiler/src/backend/llvm/lib-tls.ts @@ -11,6 +11,7 @@ import { rawBytes, requestHandler, } from "./lib-abi.js"; +import { sniCallbackArgs } from "./lib-http2.js"; import { emitAlwaysThrowLibCall } from "./lib-shared.js"; export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { @@ -43,6 +44,27 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu [...rawBytes(host, args[0]!), ...rawBytes(host, args[1]!), ...handler], ); } + case "tls.createServerSni": + case "tls.createServerSniCb": { + const cb = args[3]; + const handler = + cb === undefined + ? [ptr(), ptr()] + : callbackAdapter( + host, + cb, + callbackType(cb).params.length === 0 + ? "scr_net_conn_thunk0" + : "scr_net_conn_thunk_sock", + ["ptr", "ptr"], + ); + return finishRuntimeCall(host, e, "scr_tls_create_server_sni", [ + ...rawBytes(host, args[0]!), + ...rawBytes(host, args[1]!), + ...handler, + ...sniCallbackArgs(host, args[2]!), + ]); + } case "tls.createServerDyn": case "tls.createServerDynCb": case "https.createServerDyn": diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 4a7c00d845..c27b2513e6 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -4749,7 +4749,8 @@ function lowerTlsServerOptions( lowerer: Lowerer, node: ts.Expression, what: string, -): { cert: IrExpr; key: IrExpr } { + allowSni = false, +): { cert: IrExpr; key: IrExpr; sni?: IrExpr } { if (!ts.isObjectLiteralExpression(node)) { lowerer.noLowering( `${what} with a non-literal options argument`, @@ -4760,6 +4761,7 @@ function lowerTlsServerOptions( const loc = locOf(node); let cert: IrExpr | null = null; let key: IrExpr | null = null; + let sni: IrExpr | undefined; for (const prop of node.properties.nodes) { let initializer: ts.Expression | null; if ( @@ -4777,6 +4779,23 @@ function lowerTlsServerOptions( ); } const k = (prop.name as ts.Identifier | ts.StringLiteral).text; + if (k === "SNICallback" && allowSni) { + // The callback is a value read (a function or a binding), so it may + // be read after cert and key without changing what the program sees. + if (initializer !== null && !sideEffectFreeOptionValue(initializer)) { + lowerer.noLowering( + `a ${what} SNICallback expression with side effects`, + prop, + "pass a function or a variable that holds one", + ); + } + const value = + initializer !== null + ? lowerer.lowerExpr(initializer) + : lowerer.lowerShorthandValue(prop as ts.ShorthandPropertyAssignment); + sni = checkSniCallbackValue(lowerer, value, prop, what); + continue; + } if (k !== "cert" && k !== "key") { const init = ts.isPropertyAssignment(prop) ? prop.initializer : null; if ( @@ -4801,7 +4820,7 @@ function lowerTlsServerOptions( "cert and key (PEM strings or Buffers) are the supported options", { SNICallback: - "SNICallback lowers only on http2.createSecureServer({ allowHTTP1: true, ... }) — serve one cert/key pair here", + "SNICallback lowers on tls.createServer and http2.createSecureServer({ allowHTTP1: true, ... }) — serve one cert/key pair here", requestCert: "client-certificate handshakes are not modeled (SEMANTICS.md divergence 55) — requestCert: false is the lowered value", }, @@ -4848,7 +4867,7 @@ function lowerTlsServerOptions( "the supported options object is { cert, key } (PEM strings or Buffers)", ); } - return { cert, key }; + return sni === undefined ? { cert, key } : { cert, key, sni }; } /** The options argument of tls/https createServer, split three ways: an @@ -4911,12 +4930,24 @@ function lowerTlsServerOptionsOrDyn( what: string, includeHttpTimeoutOptions = false, runtimeKeys: ReadonlySet = new Set(), -): { cert: IrExpr; key: IrExpr; dyn?: undefined } | { dyn: IrExpr } { - if ( - ts.isObjectLiteralExpression(node) && - !tlsLiteralNeedsRuntimeWalk(node, includeHttpTimeoutOptions, runtimeKeys) - ) { - return lowerTlsServerOptions(lowerer, node, what); + allowSni = false, +): { cert: IrExpr; key: IrExpr; sni?: IrExpr; dyn?: undefined } | { dyn: IrExpr } { + if (ts.isObjectLiteralExpression(node)) { + if (!tlsLiteralNeedsRuntimeWalk(node, includeHttpTimeoutOptions, runtimeKeys)) + return lowerTlsServerOptions(lowerer, node, what, allowSni); + const sniOption = node.properties.nodes.find( + (p) => + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && + p.name.text === "SNICallback", + ); + if (allowSni && sniOption !== undefined) { + lowerer.noLowering( + `${what} with SNICallback and options the runtime walk reads`, + sniOption, + "SNICallback lowers with cert and key; ALPNProtocols, minVersion, maxVersion and the validated options are not combined with it yet", + ); + } } const v = lowerer.lowerExpr(node); if (v.type.kind === "dyn") return { dyn: v }; @@ -5078,10 +5109,25 @@ function lowerTlsModuleCall( "tls.createServer", false, TLS_SERVER_RUNTIME_OPTIONS, + true, ); if (args.length === 1) { - return opts.dyn !== undefined - ? { kind: "libCall", fn: "tls.createServerDyn", args: [opts.dyn], type: NETSERVER_T, loc } + if (opts.dyn !== undefined) + return { + kind: "libCall", + fn: "tls.createServerDyn", + args: [opts.dyn], + type: NETSERVER_T, + loc, + }; + return opts.sni !== undefined + ? { + kind: "libCall", + fn: "tls.createServerSni", + args: [opts.cert, opts.key, opts.sni], + type: NETSERVER_T, + loc, + } : { kind: "libCall", fn: "tls.createServer", @@ -5099,11 +5145,19 @@ function lowerTlsModuleCall( "use (socket) or ()", [NETSOCKET_T], ); - return opts.dyn !== undefined + if (opts.dyn !== undefined) + return { + kind: "libCall", + fn: "tls.createServerDynCb", + args: [opts.dyn, cb], + type: NETSERVER_T, + loc, + }; + return opts.sni !== undefined ? { kind: "libCall", - fn: "tls.createServerDynCb", - args: [opts.dyn, cb], + fn: "tls.createServerSniCb", + args: [opts.cert, opts.key, opts.sni, cb], type: NETSERVER_T, loc, } @@ -5481,6 +5535,44 @@ const SNI_HINT = "...(x ? { SNICallback: x } : {}) — the callback is (servername, cb) => void " + "with cb: (err: Error | null, ctx?: tls.SecureContext) => void"; +/** An SNICallback option value: (servername: string, cb) => void where cb + * is (err: Error | null, ctx?: SecureContext) => void, or that function + * unioned with undefined (no callback). cb may declare fewer params (the + * emitted answer thunk matches its ABI), but what it does declare must + * carry the arms the thunk decodes. */ +function lowerSniCallbackValue( + lowerer: Lowerer, + node: ts.Expression, + blame: ts.Node, + what: string, +): IrExpr { + return checkSniCallbackValue(lowerer, lowerer.lowerExpr(node), blame, what); +} + +function checkSniCallbackValue(lowerer: Lowerer, v: IrExpr, blame: ts.Node, what: string): IrExpr { + const shapeOk = (t: IrType): boolean => { + if (t.kind !== "func" || t.ret.kind !== "void" || t.params.length !== 2) return false; + if (t.params[0]!.kind !== "string") return false; + const cbT = t.params[1]!; + if (cbT.kind !== "func" || cbT.ret.kind !== "void" || cbT.params.length > 2) return false; + const armKinds = (u: IrType): string[] => + u.kind === "union" ? (lowerer.unions.get(u.unionId)?.arms ?? []).map((a) => a.kind) : []; + const p0 = cbT.params[0]; + if (p0 !== undefined && !armKinds(p0).includes("nullT")) return false; + const p1 = cbT.params[1]; + if (p1 !== undefined && !armKinds(p1).includes("secureCtx")) return false; + return true; + }; + const arms = v.type.kind === "union" ? (lowerer.unions.get(v.type.unionId)?.arms ?? []) : []; + const funcArm = v.type.kind === "func" ? v.type : arms.find((a) => a.kind === "func"); + const unionOk = + v.type.kind !== "union" || (arms.length === 2 && arms.some((a) => a.kind === "undefinedT")); + if (funcArm === undefined || !unionOk || !shapeOk(funcArm)) { + lowerer.noLowering(`a ${what} SNICallback of '${lowerer.fmt(v.type)}' values`, blame, SNI_HINT); + } + return v; +} + /** Strips parentheses and type assertions: the portless spelling wraps * an options spread as `...({ ... } as Record)`. */ function stripParensAndCasts(node: ts.Expression): ts.Expression { @@ -5733,43 +5825,8 @@ function lowerHttp2ModuleCall( let sni: IrExpr | null = null; let allowHttp1 = false; let enableConnectProtocol = false; - // The SNICallback value shape: (servername: string, cb) => void where - // cb is (err: Error | null, ctx?: SecureContext) => void — cb may - // declare fewer params (the emitted answer thunk matches its ABI), - // but what it does declare must carry the arms the thunk decodes. - const sniShapeOk = (t: IrType): boolean => { - if (t.kind !== "func" || t.ret.kind !== "void" || t.params.length !== 2) return false; - if (t.params[0]!.kind !== "string") return false; - const cbT = t.params[1]!; - if (cbT.kind !== "func" || cbT.ret.kind !== "void" || cbT.params.length > 2) return false; - const armKinds = (u: IrType): string[] => - u.kind === "union" ? (lowerer.unions.get(u.unionId)?.arms ?? []).map((a) => a.kind) : []; - const p0 = cbT.params[0]; - if (p0 !== undefined && !armKinds(p0).includes("nullT")) return false; - const p1 = cbT.params[1]; - if (p1 !== undefined && !armKinds(p1).includes("secureCtx")) return false; - return true; - }; const lowerSniValue = (node: ts.Expression, blame: ts.Node): void => { - const v = lowerer.lowerExpr(node); - const funcArm = - v.type.kind === "func" - ? v.type - : v.type.kind === "union" - ? (lowerer.unions.get(v.type.unionId)?.arms ?? []).find((a) => a.kind === "func") - : undefined; - const unionOk = - v.type.kind !== "union" || - ((lowerer.unions.get(v.type.unionId)?.arms ?? []).length === 2 && - (lowerer.unions.get(v.type.unionId)?.arms ?? []).some((a) => a.kind === "undefinedT")); - if (funcArm === undefined || !unionOk || !sniShapeOk(funcArm)) { - lowerer.noLowering( - `a createSecureServer SNICallback of '${lowerer.fmt(v.type)}' values`, - blame, - SNI_HINT, - ); - } - sni = v; + sni = lowerSniCallbackValue(lowerer, node, blame, "createSecureServer"); }; const lowerOption = (prop: ts.ObjectLiteralElementLike): void => { if (ts.isSpreadAssignment(prop)) { diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 9f383c695e..da045a6f53 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2110,6 +2110,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * surface — the handles are the same kinds. */ "tls.createServer": { argTypes: [null, null], result: NETSERVER_T }, "tls.createServerCb": { argTypes: [null, null, null], result: NETSERVER_T }, + /** The SNICallback forms: (cert, key, SNICallback[, handler]). The + * callback is http2.createSecureServerSni's shape, a function or a + * function | undefined union. */ + "tls.createServerSni": { argTypes: [null, null, null], result: NETSERVER_T }, + "tls.createServerSniCb": { argTypes: [null, null, null, null], result: NETSERVER_T }, // The runtime options records (divergence 66's stance): a dyn options // value whose members read at runtime; pemDyn extracts a runtime-valued // cert/key member (arg 1 is the precomposed fence label). diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6933-tls-sni-callback.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6933-tls-sni-callback.ts.json new file mode 100644 index 0000000000..4cf80592f9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6933-tls-sni-callback.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6933-tls-sni-callback.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6934-tls-handshake-disconnect.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6934-tls-handshake-disconnect.ts.json new file mode 100644 index 0000000000..ae362a4231 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6934-tls-handshake-disconnect.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6934-tls-handshake-disconnect.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/tls-sni-callback-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/tls-sni-callback-fences.ts.json new file mode 100644 index 0000000000..ee2ea72f02 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/tls-sni-callback-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/tls-sni-callback-fences.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_error.c b/packages/runtime/src/scr_error.c index 94523fc43f..b974474bfa 100644 --- a/packages/runtime/src/scr_error.c +++ b/packages/runtime/src/scr_error.c @@ -409,13 +409,15 @@ SCR_TL ScrError *scr_event_error_payload = NULL; /* The code Node attaches to the runtime's event-error messages whose text * carries no errno name: connResetException's "socket hang up" and - * "aborted", the HTTP parser's header overflow, and the TLS verification - * failures (the openssl reason Node reports as the code). NULL for any + * "aborted", the TLS client's disconnect before the handshake completes, + * the HTTP parser's header overflow, and the TLS verification failures (the openssl reason Node reports as the code). NULL for any * other message. */ const char *scr_error_message_code(const ScrStr *msg) { static const struct { const char *prefix; bool whole; const char *code; } known[] = { { "socket hang up", true, "ECONNRESET" }, { "aborted", true, "ECONNRESET" }, + { "Client network socket disconnected before secure TLS connection was established", true, + "ECONNRESET" }, { "Parse Error: Header overflow", true, "HPE_HEADER_OVERFLOW" }, { "certificate has expired", true, "CERT_HAS_EXPIRED" }, { "self-signed certificate in certificate chain", true, "SELF_SIGNED_CERT_IN_CHAIN" }, diff --git a/packages/runtime/src/scr_tls.c b/packages/runtime/src/scr_tls.c index 526dd7760c..b4d125cb28 100644 --- a/packages/runtime/src/scr_tls.c +++ b/packages/runtime/src/scr_tls.c @@ -1057,10 +1057,10 @@ static int scr_tls_handshake(void *tctx) { if (r == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) { scr_tls_verify_msg(t, msg, sizeof msg); } else if (r == MBEDTLS_ERR_SSL_CONN_EOF || r == MBEDTLS_ERR_NET_CONN_RESET) { - /* the peer died mid-handshake: the http layer's own premature-close - * shape ('socket hang up') is Node's message — tear down without an error - * at this level and let the closed hook classify it */ - scr_net_sock_transport_error(t->sock, NULL); + /* The peer closed or reset the connection mid-handshake: Node's + * ECONNRESET for tls.connect and https.request alike. */ + scr_net_sock_transport_error( + t->sock, "Client network socket disconnected before secure TLS connection was established"); return -1; } else if (r == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE && t->ssl.MBEDTLS_PRIVATE(in_msg) != NULL && @@ -1170,8 +1170,15 @@ static ScrNetServer *scr_tls_create_server_alpn(const char *cert, size_t cert_le const char *key, size_t key_len, ScrClosure *handler /*moves, nullable*/, ScrNetConnFn fn, char **alpn /*moves, nullable*/, - int min_version, int max_version) { + int min_version, int max_version, + ScrClosure *sni_cb /*moves, nullable*/, + void *sni_answer_fn) { ScrTlsSrv *srv = scr_tls_srv_new(cert, cert_len, key, key_len, false); + if (sni_cb != NULL) { + srv->sni_cb = sni_cb; /* moves; released with srv */ + srv->sni_answer_fn = sni_answer_fn; + mbedtls_ssl_conf_sni(&srv->conf, &scr_tls_f_sni, NULL); + } srv->max_tls12 = max_version == 12; if (!scr_tls_conf_versions(&srv->conf, min_version, max_version)) { /* Node's server then fails every handshake; nothing here models @@ -1198,7 +1205,21 @@ static ScrNetServer *scr_tls_create_server_alpn(const char *cert, size_t cert_le ScrNetServer *scr_tls_create_server(const char *cert, size_t cert_len, const char *key, size_t key_len, ScrClosure *handler /*moves, nullable*/, ScrNetConnFn fn) { - return scr_tls_create_server_alpn(cert, cert_len, key, key_len, handler, fn, NULL, 12, 13); + return scr_tls_create_server_alpn(cert, cert_len, key, key_len, handler, fn, NULL, 12, 13, NULL, + NULL); +} + +/* tls.createServer({ cert, key, SNICallback }): the pre-parse machinery + * (design note atop the SNI section) hands each ClientHello's servername + * to the callback and serves the context it answers, or the default pair + * for none. A NULL sni_cb (the callback union's undefined arm) is + * scr_tls_create_server. */ +ScrNetServer *scr_tls_create_server_sni(const char *cert, size_t cert_len, const char *key, + size_t key_len, ScrClosure *handler /*moves, nullable*/, + ScrNetConnFn fn, ScrClosure *sni_cb /*moves, nullable*/, + void *sni_answer_fn) { + return scr_tls_create_server_alpn(cert, cert_len, key, key_len, handler, fn, NULL, 12, 13, sni_cb, + sni_answer_fn); } ScrNetServer *scr_https_create_server(const char *cert, size_t cert_len, const char *key, @@ -2024,7 +2045,7 @@ ScrNetServer *scr_tls_create_server_dyn(const ScrDyn *opts /*borrowed*/, } ScrNetServer *s = scr_tls_create_server_alpn((const char *)cert->data, cert->len, (const char *)key->data, key->len, handler, fn, - alpn, min_version, max_version); + alpn, min_version, max_version, NULL, NULL); scr_bytes_release(cert); scr_bytes_release(key); return s; diff --git a/packages/runtime/src/scr_tls.h b/packages/runtime/src/scr_tls.h index 0b1714eb33..fcb221b428 100644 --- a/packages/runtime/src/scr_tls.h +++ b/packages/runtime/src/scr_tls.h @@ -15,6 +15,9 @@ * (port 443 default, `ca` / rejectUnauthorized for local-CA flows — * cert/key/ca all arrive as borrowed PEM bytes). */ ScrNetServer *scr_tls_create_server(const char *cert, size_t cert_len, const char *key, size_t key_len, ScrClosure *handler /*moves, nullable*/, ScrNetConnFn fn); /* +1 */ +/* The SNICallback form: sni_cb (moves, nullable) receives (servername, answer), and + * sni_answer_fn is the emitted thunk that hands the answer to scr_tls_sni_answer. */ +ScrNetServer *scr_tls_create_server_sni(const char *cert, size_t cert_len, const char *key, size_t key_len, ScrClosure *handler /*moves, nullable*/, ScrNetConnFn fn, ScrClosure *sni_cb /*moves, nullable*/, void *sni_answer_fn); /* +1 */ /* ── RUNTIME tls/https options records (the divergence-66 stance: a * non-literal options value reads its members at runtime; members whose diff --git a/tests/corpus/6933-tls-sni-callback.ts b/tests/corpus/6933-tls-sni-callback.ts new file mode 100644 index 0000000000..7958882d54 --- /dev/null +++ b/tests/corpus/6933-tls-sni-callback.ts @@ -0,0 +1,65 @@ +// tls.createServer with an SNICallback: the callback receives each +// ClientHello's servername and answers a SecureContext synchronously or +// later, or none, so the default cert/key pair serves; an error answer +// fails that handshake. Clients that trust the CA see the certificate the +// answer chose: alt.localhost verifies only with the alt context, and a +// name served the default localhost pair fails Node's hostname check. +import { readFileSync } from "node:fs"; +import * as net from "node:net"; +import * as tls from "node:tls"; + +const certs = "tests/fixtures/server/certs"; +const ca = readFileSync(`${certs}/ca.pem`); +const cert = readFileSync(`${certs}/localhost.pem`); +const key = readFileSync(`${certs}/localhost-key.pem`); +const altContext = tls.createSecureContext({ + cert: readFileSync(`${certs}/alt.pem`), + key: readFileSync(`${certs}/alt-key.pem`), +}); + +const server = tls.createServer( + { + cert, + key, + SNICallback: (servername: string, cb: (err: Error | null, ctx?: tls.SecureContext) => void) => { + console.log("sni", servername); + if (servername === "alt.localhost") setTimeout(() => cb(null, altContext), 5); + else if (servername === "fail.localhost") cb(new Error(`no certificate for ${servername}`)); + else cb(null, undefined); + }, + }, + (socket) => { + socket.end("hello"); + }, +); + +function attempt(port: number, servername: string, next: () => void): void { + let done = false; + const finish = (line: string): void => { + if (done) return; + done = true; + console.log(servername, line); + next(); + }; + const socket = tls.connect({ port, host: "127.0.0.1", servername, ca }, () => { + let text = ""; + socket.on("data", (data: Buffer) => { + text += data.toString(); + }); + socket.on("end", () => finish(`authorized ${socket.authorized} ${text}`)); + }); + socket.on("error", (error: Error) => { + finish(`error ${(error as NodeJS.ErrnoException).code}`); + }); +} + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + attempt(port, "localhost", () => + attempt(port, "alt.localhost", () => + attempt(port, "other.localhost", () => + attempt(port, "fail.localhost", () => server.close(() => console.log("closed"))), + ), + ), + ); +}); diff --git a/tests/corpus/6934-tls-handshake-disconnect.ts b/tests/corpus/6934-tls-handshake-disconnect.ts new file mode 100644 index 0000000000..1f13c66844 --- /dev/null +++ b/tests/corpus/6934-tls-handshake-disconnect.ts @@ -0,0 +1,27 @@ +// A server that closes the connection before the TLS handshake completes: +// tls.connect and https.get fail with ECONNRESET and Node's "Client network +// socket disconnected before secure TLS connection was established". +import * as https from "node:https"; +import * as net from "node:net"; +import * as tls from "node:tls"; + +const server = net.createServer((socket) => { + socket.end(); +}); + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const client = tls.connect({ port, host: "127.0.0.1", rejectUnauthorized: false }, () => { + console.log("tls connected"); + }); + client.on("error", (error: Error) => { + console.log("tls", (error as NodeJS.ErrnoException).code, error.message); + const request = https.get({ host: "127.0.0.1", port, rejectUnauthorized: false }, () => { + console.log("https response"); + }); + request.on("error", (err: Error) => { + console.log("https", (err as NodeJS.ErrnoException).code, err.message); + server.close(); + }); + }); +}); diff --git a/tests/diagnostics/tls-sni-callback-fences.ts b/tests/diagnostics/tls-sni-callback-fences.ts new file mode 100644 index 0000000000..0c745365cd --- /dev/null +++ b/tests/diagnostics/tls-sni-callback-fences.ts @@ -0,0 +1,15 @@ +// tls.createServer SNICallback forms that have no lowering. +import { readFileSync } from "node:fs"; +import * as tls from "node:tls"; + +const cert = readFileSync("cert.pem"); +const key = readFileSync("key.pem"); +type Answer = (err: Error | null, ctx?: tls.SecureContext) => void; +function makeSni(): (servername: string, cb: Answer) => void { + return (_servername, cb) => cb(null, undefined); +} + +// The callback expression has side effects. +tls.createServer({ cert, key, SNICallback: makeSni() }, (socket) => socket.end()); +// SNICallback with options the runtime options walk reads. +tls.createServer({ cert, key, ALPNProtocols: ["h2"], SNICallback: makeSni }); diff --git a/tests/harness/__snapshots__/tls-sni-callback-fences.ts.txt b/tests/harness/__snapshots__/tls-sni-callback-fences.ts.txt new file mode 100644 index 0000000000..920e655323 --- /dev/null +++ b/tests/harness/__snapshots__/tls-sni-callback-fences.ts.txt @@ -0,0 +1,17 @@ +tls-sni-callback-fences.ts:13:31 - error SC2020: 'a tls.createServer SNICallback expression with side effects' is part of the standard library types but has no scriptc lowering yet + + 12 | // The callback expression has side effects. + 13 | tls.createServer({ cert, key, SNICallback: makeSni() }, (socket) => socket.end()); + | ^~~~~~~~~~~~~~~~~~~~~~ + 14 | // SNICallback with options the runtime options walk reads. + + hint: pass a function or a variable that holds one + +tls-sni-callback-fences.ts:15:54 - error SC2020: 'tls.createServer with SNICallback and options the runtime walk reads' is part of the standard library types but has no scriptc lowering yet + + 14 | // SNICallback with options the runtime options walk reads. + 15 | tls.createServer({ cert, key, ALPNProtocols: ["h2"], SNICallback: makeSni }); + | ^~~~~~~~~~~~~~~~~~~~ + 16 | + + hint: SNICallback lowers with cert and key; ALPNProtocols, minVersion, maxVersion and the validated options are not combined with it yet \ No newline at end of file From 644f42373471845ab82d9ea9c66bb472ece533bc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 14:22:51 -0500 Subject: [PATCH 007/395] Evaluate fs arguments up to the last one with effects before a URL path conversion A synchronous fs call with a URL path evaluated only the later arguments with effects ahead of the path conversion. When such an argument reassigned the variable an earlier argument reads, as in fs.chownSync(url, uid, next()), the earlier argument was read after the reassignment. Every argument from the URL through the last one with effects is now evaluated in source order before the conversion. The npm fixture node24-fs-descriptors now reaches its SC2020 refusal for writeSync with a hex encoding, since openSync accepts its file URL. --- .../frontend/lowering/builtins/fs-paths.ts | 31 +++++++++++++------ tests/corpus/6931-fs-url-paths.ts | 12 +++++++ tests/harness/npm.test.ts | 2 +- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts b/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts index fb9176b53d..181e27e34a 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts @@ -2,9 +2,10 @@ * fileURLToPath inside each function (getValidatedPath), after every * argument has been evaluated, and validates some arguments before the * path and others after it. A URL-typed path lowers to the same - * conversion (url.fileURLToPathUrl). Later arguments with side effects are - * evaluated into hidden locals before the conversion, so they run even - * when it throws. The conversion keeps Node's error order when every + * conversion (url.fileURLToPathUrl). The later arguments through the last + * one with side effects are evaluated into hidden locals before the + * conversion, in source order, so they run even when it throws and each + * reads what it reads in Node. The conversion keeps Node's error order when every * earlier path is a URL too and each argument Node validates before the * path is absent or a literal that passes that validation; other URL * forms fence with SC2020, as do the fs.promises functions, whose @@ -134,16 +135,26 @@ function checkUrlPaths( // path Node checks first cannot precede the URL. if (row.paths.some((i) => i < first && args[i] !== undefined)) lowerer.noLowering(`${name} with a string path before a URL path`, call, hint); + // Every argument from the URL through the last one with effects is + // evaluated ahead of the conversion, in source order: a later argument's + // effect (an assignment, say) must not change what an earlier one read. + let last = first; + for (let i = first + 1; i < args.length; i++) if (!sideEffectFreeOptionValue(args[i]!)) last = i; const staged: ts.Expression[] = []; - for (let i = first + 1; i < args.length; i++) { - const arg = args[i]!; - if (sideEffectFreeOptionValue(arg)) continue; + for (let i = first + 1; i <= last; i++) { + let e = args[i]!; + while (ts.isParenthesizedExpression(e)) e = e.expression; // Literals are read by their members, so a whole-value slot cannot // hold them. - let e = arg; - while (ts.isParenthesizedExpression(e)) e = e.expression; - if (ts.isObjectLiteralExpression(e) || ts.isArrayLiteralExpression(e)) - lowerer.noLowering(`${name} with a URL path and a later literal that has effects`, arg, hint); + if (ts.isObjectLiteralExpression(e) || ts.isArrayLiteralExpression(e)) { + lowerer.noLowering( + sideEffectFreeOptionValue(e) + ? `${name} with a URL path and a literal before a later argument that has effects` + : `${name} with a URL path and a later literal that has effects`, + e, + hint, + ); + } staged.push(e); } if (staged.length > 0) { diff --git a/tests/corpus/6931-fs-url-paths.ts b/tests/corpus/6931-fs-url-paths.ts index 91a630d1bb..cf68890669 100644 --- a/tests/corpus/6931-fs-url-paths.ts +++ b/tests/corpus/6931-fs-url-paths.ts @@ -18,6 +18,13 @@ function mode(): number { console.log("evaluated mode"); return 0o600; } +// A later argument that reassigns an earlier argument's variable runs after +// that argument was read. +let uid = -1; +function bumpUid(): number { + uid = 123456; + return -1; +} function clean(text: string): string { return text.split(dir).join(""); @@ -91,6 +98,11 @@ attempt("mkdir", () => { attempt("later effects", () => fs.copyFileSync(new URL("http://a/"), logged("dest.txt"))); attempt("later mode", () => fs.chmodSync(new URL("http://a/"), mode())); attempt("chmod", () => fs.chmodSync(new URL("data:text/plain,a"), 0o600)); +attempt("chown order", () => fs.chownSync(file, uid, bumpUid())); +console.log("uid", uid); +uid = -1; +attempt("chown order scheme", () => fs.chownSync(new URL("http://a/"), uid, bumpUid())); +console.log("uid", uid); fs.rmSync(dir, { recursive: true, force: true }); console.log("done", fs.existsSync(dir)); diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 9ab93f2870..2df0d87ef3 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -72,7 +72,7 @@ const KNOWN: Record = { "node24-esm-metadata": { run: "SC1090" }, "node24-event-emitter": { run: "SC2020" }, "node24-file-url-conversion": { build: "SC1090" }, - "node24-fs-descriptors": { run: "SC1090" }, + "node24-fs-descriptors": { run: "SC2020" }, "node24-mime": { run: "SC2020" }, "node24-mime-errors": { run: "SC1090" }, "node24-no-error-assertions": { run: "SC2020" }, From eca349d2440b2c4274823b952d6201022992b9c3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 15:32:52 -0500 Subject: [PATCH 008/395] Forward nested zlib callback results and keep self-calling callbacks' error checks A nested callback lowered inside an outer error path passed the absent Buffer on into a Buffer slot even when its own operation succeeded, so a callee such as (err, out) => cb(err, out) received undefined instead of the result. The forwarded value is now the Buffer when there is one. A named function expression passed to zlib can call itself, and a function expression can reach itself through arguments.callee, with an error and a Buffer. Such functions no longer count as called only by the adapter, so their success copy keeps the runtime error checks instead of folding them to the success case. --- .../frontend/lowering/builtins/arguments.ts | 8 ++++- .../src/frontend/lowering/lower-functions.ts | 24 ++++++++++----- .../corpus/6920-zlib-callback-error-result.ts | 29 +++++++++++++++++++ .../6920-zlib-callback-error-result.ts.json | 2 +- 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/builtins/arguments.ts b/packages/compiler/src/frontend/lowering/builtins/arguments.ts index d3bf467749..7f862f6031 100644 --- a/packages/compiler/src/frontend/lowering/builtins/arguments.ts +++ b/packages/compiler/src/frontend/lowering/builtins/arguments.ts @@ -20,6 +20,7 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; import { lowerAbsenceProbe } from "../lower-exprs.js"; +import { bodyReadsArguments } from "../../arguments-usage.js"; import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "../optional-arguments.js"; import { voidizedCallback } from "../lower-server.js"; export function optionalStringTags( @@ -269,9 +270,14 @@ export function errorFirstCallback( ): IrExpr { // A literal here is called only by the callback adapter, which passes // null as the error whenever it passes a result (lowerUndefinedResultBody). + // A named function expression can also call itself, and a function + // expression can reach itself through `arguments.callee`. let literal: ts.Expression = node; while (ts.isParenthesizedExpression(literal)) literal = literal.expression; - if (ts.isArrowFunction(literal) || ts.isFunctionExpression(literal)) + if ( + ts.isArrowFunction(literal) || + (ts.isFunctionExpression(literal) && literal.name === undefined && !bodyReadsArguments(literal)) + ) lowerer.errorFirstLiterals.add(literal); let callback = lowerer.lowerExpr(node); if (callback.type.kind === "dyn") { diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 9254e62134..6fc083468f 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -718,13 +718,13 @@ function lowerUndefinedResultBody( const loc = locOf(node); const param = lowerer.bindingIn(lowerer.ctx, result.symbol); if (param === null) throw new InternalCompilerError("lowerer bug: callback result unbound"); - const absent: IrExpr = { + const absent = (): IrExpr => ({ kind: "libCall", fn: "bytes.isAbsent", args: [varRef(param.id, param.type, loc)], type: BOOL, loc, - }; + }); const fence: IrStmt = { kind: "runtimeFence", code: "SC2020", @@ -750,9 +750,10 @@ function lowerUndefinedResultBody( lowerer.suppressStats = true; lowerer.errorPathCopies++; try { - // Passed on into a Buffer slot, the result stays the absent Buffer - // the adapter passed: a callee with this entry test reads it as - // undefined. + // Passed on into a Buffer slot, the result stays what the adapter + // passed: the Buffer, or the absent Buffer, which a callee with this + // entry test reads as undefined. (The copy that serves both paths + // reaches this with a Buffer.) const body = transformStmtList(withKnownError(knownError, lowerBody), { expr: (e) => e.kind === "dynCheck" && @@ -760,7 +761,14 @@ function lowerUndefinedResultBody( e.value.localId === slot.id && e.type.kind === "bytes" && e.type.elem === "u8" - ? { kind: "libCall", fn: "bytes.absent", args: [], type: e.type, loc: e.loc } + ? { + kind: "ternary", + cond: absent(), + then: { kind: "libCall", fn: "bytes.absent", args: [], type: e.type, loc: e.loc }, + else_: varRef(param.id, param.type, e.loc), + type: e.type, + loc: e.loc, + } : e, stmt: (stmt) => stmt, }); @@ -795,7 +803,7 @@ function lowerUndefinedResultBody( localId: slot.id, init: { kind: "ternary", - cond: absent, + cond: absent(), then: dynUndefinedExpr(loc), else_: lowerer.coerceInto(result.name, varRef(param.id, param.type, loc), DYN), type: DYN, @@ -811,7 +819,7 @@ function lowerUndefinedResultBody( return [ { kind: "if", - cond: absent, + cond: absent(), then: withoutResult === null ? [fence] : [undefinedResult, ...withoutResult], else_: withResult, loc, diff --git a/tests/corpus/6920-zlib-callback-error-result.ts b/tests/corpus/6920-zlib-callback-error-result.ts index 50998d37f6..cc91282213 100644 --- a/tests/corpus/6920-zlib-callback-error-result.ts +++ b/tests/corpus/6920-zlib-callback-error-result.ts @@ -149,6 +149,33 @@ function inflated(input: Buffer): Promise { ); } +function sink(err: Error | null, out: Buffer): void { + console.log("sink", err, typeof out, out?.length); + step(); +} +function nestedForward(): void { + // On the outer error path, the nested callback forwards its own result. + zlib.inflate(bad, (err, out) => { + console.log("outer", err?.code, typeof out); + zlib.inflate(good, (err2, out2) => sink(err2, out2)); + }); +} +let attempts = 0; +function namedRetry(): void { + // A named callback that calls itself with an error and a Buffer. + zlib.inflate(bad, function retry(err, out) { + attempts++; + if (err) { + if (attempts < 3) return retry(new Error("again"), Buffer.from("x")); + console.log("gave up", attempts, err.message, out.length); + step(); + return; + } + console.log("retry value", out.length); + step(); + }); +} + function functionValues(next: () => void): void { steps.push( () => zlib.inflate(good, named), @@ -174,6 +201,8 @@ function functionValues(next: () => void): void { () => forwardGzip("text that does not fit", arrow), () => zlib.inflate(bad, captures), () => zlib.inflate(good, captures), + nestedForward, + namedRetry, () => { named(new Error("direct"), Buffer.from("both")); }, diff --git a/tests/harness/corpus-costs/6920-zlib-callback-error-result.ts.json b/tests/harness/corpus-costs/6920-zlib-callback-error-result.ts.json index 1241a31c06..247292295e 100644 --- a/tests/harness/corpus-costs/6920-zlib-callback-error-result.ts.json +++ b/tests/harness/corpus-costs/6920-zlib-callback-error-result.ts.json @@ -1 +1 @@ -{ "plain": 5517, "san": 7607 } +{ "plain": 6386, "san": 8023 } From 238b08352b3b1f77c071116bc5dc6c1ff864bd4e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 17:00:19 -0500 Subject: [PATCH 009/395] Keep named properties written to arrays through checked views --- .../src/backend/llvm/expr-stream-bridges.ts | 3 + .../compiler/src/backend/llvm/lib-dispatch.ts | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/backend/llvm/reference-writes.ts | 1 + .../src/frontend/lowering/lower-stmts.ts | 82 ++++++++++++++++++- .../compiler/src/ir/builtin-signatures.ts | 3 + packages/compiler/src/ir/validate.ts | 6 ++ .../6987-array-view-named-properties.ts.json | 6 ++ packages/runtime/src/scr_array.c | 64 ++++++++++++++- packages/runtime/src/scr_array.h | 9 ++ packages/runtime/src/scr_json.c | 17 ++++ .../6987-array-view-named-properties.ts | 79 ++++++++++++++++++ .../6987-array-view-named-properties.ts.json | 1 + 13 files changed, 266 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6987-array-view-named-properties.ts.json create mode 100644 tests/corpus/6987-array-view-named-properties.ts create mode 100644 tests/harness/corpus-costs/6987-array-view-named-properties.ts.json diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index ce33aa2cd2..b72875aaa8 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -176,6 +176,7 @@ export function streamTypedRefCommitAdapter( const check = host.dyn.dynCheckHelper(t); host.declare(`declare void @scr_arr_release(ptr)`); host.declare(`declare void @scr_arr_outline(ptr)`); + host.declare(`declare void @scr_arr_adopt_view_properties(ptr, ptr, ptr)`); const lines = [ `define internal void @${commit}(ptr %target, ptr %d) ${FN_ATTRS} { ; commit live stream element ${typeKey(t)}`, `entry:`, @@ -183,6 +184,8 @@ export function streamTypedRefCommitAdapter( ` %missing = icmp eq ptr %next, null`, ` br i1 %missing, label %done, label %swap`, `swap:`, + // Properties move with the storage (in its out-of-line part). + ` call void @scr_arr_adopt_view_properties(ptr %next, ptr %d, ptr %target)`, // Inline slots live in each header's block; move them out first. ` call void @scr_arr_outline(ptr %target)`, ` call void @scr_arr_outline(ptr %next)`, diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 924027ee29..1868ca4022 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -864,6 +864,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "events": case "blob": case "wasm": + case "array": case "record": case "disposable": case "punycode": diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index feb499203a..be7a4973b1 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1519,6 +1519,7 @@ export const LIB_FN_SYMS: Record = { "regex.groupNumber": "scr_regex_group_number", "regex.groupParseInt": "scr_regex_group_parse_int", "regex.resultProp": "scr_regex_result_prop", + "array.namedKeys": "scr_arr_named_keys", // 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 a0a153da18..c77a5a191c 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -105,6 +105,7 @@ const INERT_LIB_CALLS = new Set([ "regex.groupNumber", "regex.groupParseInt", "regex.resultProp", + "array.namedKeys", "process.cwd", "process.argv", "process.execArgv", diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index b04ba0b098..0960b9d6aa 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -11594,10 +11594,23 @@ function lowerForInArray( try { const arr = lowerer.declareHiddenLocal("%inarr", arrExpr.type); const len = lowerer.declareHiddenLocal("%inlen", F64); + const keysT: IrType = { kind: "array", elem: STRING }; + const keys = lowerer.declareHiddenLocal("%inkeys", keysT); + const total = lowerer.declareHiddenLocal("%intotal", F64); const i = lowerer.declareHiddenLocal("%ini", F64); i.mutable = true; const arrRef = (): IrExpr => ({ kind: "varRef", localId: arr.id, type: arrExpr.type, loc }); + const keysRef = (): IrExpr => ({ kind: "varRef", localId: keys.id, type: keysT, loc }); + const lenRef = (): IrExpr => ({ kind: "varRef", localId: len.id, type: F64, loc }); const iRef = (): IrExpr => ({ kind: "varRef", localId: i.id, type: F64, loc }); + const isIndex = (): IrExpr => ({ + kind: "bin", + op: "<", + left: iRef(), + right: lenRef(), + type: BOOL, + loc, + }); const liveLen = (): IrExpr => ({ kind: "arrIntrinsic", method: "length", @@ -11606,7 +11619,44 @@ function lowerForInArray( type: F64, loc, }); - const keyInit: IrExpr = { kind: "toString", operand: iRef(), type: STRING, loc }; + // Indices first, then the named keys the array had when the loop began. + const keyInit: IrExpr = { + kind: "ternary", + cond: isIndex(), + then: { kind: "toString", operand: iRef(), type: STRING, loc }, + else_: { + kind: "arrayGet", + arr: keysRef(), + index: { kind: "bin", op: "-", left: iRef(), right: lenRef(), type: F64, loc }, + type: STRING, + loc, + }, + type: STRING, + loc, + }; + // An index is visited while it is below the live length and not a hole. + const visits: IrExpr = { + kind: "ternary", + cond: isIndex(), + then: { + kind: "logical", + op: "&&", + left: { kind: "bin", op: "<", left: iRef(), right: liveLen(), type: BOOL, loc }, + right: { + kind: "bin", + op: "!==", + left: { kind: "arrayState", arr: arrRef(), index: iRef(), type: F64, loc }, + right: { kind: "numLit", value: 0, type: F64, loc }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + else_: { kind: "boolLit", value: true, type: BOOL, loc }, + type: BOOL, + loc, + }; // The three binding forms, sharing the per-visit key declaration. let keyDecl: IrStmt; @@ -11670,6 +11720,32 @@ function lowerForInArray( body: [ { kind: "varDecl", localId: arr.id, init: arrExpr, loc }, { kind: "varDecl", localId: len.id, init: liveLen(), loc }, + { + kind: "varDecl", + localId: keys.id, + init: { kind: "libCall", fn: "array.namedKeys", args: [arrRef()], type: keysT, loc }, + loc, + }, + { + kind: "varDecl", + localId: total.id, + init: { + kind: "bin", + op: "+", + left: lenRef(), + right: { + kind: "arrIntrinsic", + method: "length", + receiver: keysRef(), + args: [], + type: F64, + loc, + }, + type: F64, + loc, + }, + loc, + }, { kind: "for", init: { @@ -11682,7 +11758,7 @@ function lowerForInArray( kind: "bin", op: "<", left: iRef(), - right: { kind: "varRef", localId: len.id, type: F64, loc }, + right: { kind: "varRef", localId: total.id, type: F64, loc }, type: BOOL, loc, }, @@ -11702,7 +11778,7 @@ function lowerForInArray( body: [ { kind: "if", - cond: { kind: "bin", op: "<", left: iRef(), right: liveLen(), type: BOOL, loc }, + cond: visits, then: [keyDecl, ...writes, ...body], else_: null, loc, diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 9f383c695e..1e62703660 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3724,6 +3724,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "regex.groupParseInt": { argTypes: [arrayOf(STRING), F64, F64], result: F64 }, // `.index` / `.input` (args[1]) of a match result. Borrowed; never throws. "regex.resultProp": { argTypes: [arrayOf(STRING), STRING], result: DYN }, + // The string keys `for...in` visits on an array (args[0], any element + // type) after its indices. Borrowed; never throws. + "array.namedKeys": { argTypes: [null], result: arrayOf(STRING) }, // 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 f562d20e06..0004be761c 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4262,6 +4262,12 @@ function validateFunction( if (!ok) err(`libCall dyn.objEntryPairs must return a [string, any] tuple array`, e.loc); return true; } + if (e.fn === "array.namedKeys") { + if (e.args[0]?.type.kind !== "array") { + err(`libCall array.namedKeys arg 0: expected an array, got ${e.args[0]?.type.kind}`, e.loc); + } + return true; + } if (e.fn === "fetch.streamFrom") { const t = e.args[0]?.type; const ok = diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6987-array-view-named-properties.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6987-array-view-named-properties.ts.json new file mode 100644 index 0000000000..c121264533 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6987-array-view-named-properties.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6987-array-view-named-properties.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index f8b5813a22..fa98dd38a3 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -4795,11 +4795,67 @@ void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) { if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE && scr_arr_match_copy(a, target)) return; - if (!SCR_AX(a)->metadata) return; - for (size_t i = 0; i < SCR_AX(a)->metadata->v.obj.len; i++) { - ScrDynEntry *entry = &SCR_AX(a)->metadata->v.obj.entries[i]; + 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); - scr_dyn_key_set(target, key, entry->value); + 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); } } + +ScrArr *scr_arr_named_keys(ScrArr *a) { + /* Most arrays have no out-of-line storage: they share one immortal empty + * result, which `for...in` only reads. */ + static ScrArr none = {.rc = SIZE_MAX, .elem = SCR_ELEM_STR}; + const ScrArrExt *x = scr_arr_ext_of(a); + if (!x) return &none; + ScrArr *keys = scr_arr_new(SCR_ELEM_STR, 0); + for (size_t i = 0; i < x->prop_len; i++) + scr_arr_push_ref(keys, scr_str_new(x->props[i].key, x->props[i].key_len)); + const ScrDyn *metadata = x->metadata; + if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE) { + static const char *const names[] = {"index", "input", "groups"}; + for (size_t i = 0; i < 3; i++) scr_arr_push_ref(keys, scr_str_new(names[i], strlen(names[i]))); + return keys; + } + if (!metadata || metadata->kind != SCR_DYN_OBJ) return keys; + for (size_t i = 0; i < metadata->v.obj.len; i++) { + const ScrDynEntry *entry = &metadata->v.obj.entries[i]; + if (!entry->enumerable || scr_arr_prop_index(a, entry->key, entry->key_len) != SIZE_MAX) continue; + scr_arr_push_ref(keys, scr_str_new(entry->key, entry->key_len)); + } + return keys; +} + +void scr_arr_adopt_view_properties(ScrArr *a, ScrDyn *view, ScrArr *target) { + ScrArrExt *from = scr_arr_ext_of(target); + if (from && from->prop_len) { + /* Both arrays have the same element type, so the slots keep their owner. */ + ScrArrExt *x = scr_arr_ext(a); + ScrArrProp *props = x->props; + size_t len = x->prop_len, cap = x->prop_cap; + x->props = from->props; + x->prop_len = from->prop_len; + x->prop_cap = from->prop_cap; + from->props = props; + from->prop_len = len; + from->prop_cap = cap; + } + ScrDyn *named = view && view->kind == SCR_DYN_ARR ? view->v.arr.properties : NULL; + if (!named || named->v.obj.len == 0) return; + ScrArrExt *x = scr_arr_ext(a); + ScrDyn *old = x->metadata; + x->metadata = scr_dyn_retain(named); + scr_dyn_release(old); +} diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 2068a324d3..d9f5f49bc1 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -317,6 +317,15 @@ static inline ScrArr *scr_arr_retain(ScrArr *a) { * the candidate-root buffer like every headered type. */ void scr_arr_release(ScrArr *a); void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target); +/* The commit of a live array view calls this on `a`, the array converted + * from `view`, before `target` takes its storage: `a` takes the view's named + * (non-index) properties as its metadata and the noncanonical numeric + * properties of `target`, so neither is lost when the storage is swapped. */ +void scr_arr_adopt_view_properties(ScrArr *a, struct ScrDyn *view, ScrArr *target); +/* The enumerable string keys `for...in` visits after the indices: the + * noncanonical numeric properties, then the named properties in insertion + * order. +1. */ +ScrArr *scr_arr_named_keys(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_json.c b/packages/runtime/src/scr_json.c index adcc96560d..8577a96fb8 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -10269,6 +10269,23 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) { } return; } + /* A live native array keeps its named properties in its metadata, so + * deleting one goes through the view and its commit. Elements and + * `length` keep the boundary below. */ + size_t element; + if (recv->kind == SCR_DYN_TYPED_REF && recv->v.typed_ref.commit && scr_dyn_typed_ref_array_ops(recv) && + !scr_typed_ref_is_length(key) && !scr_typed_ref_canonical_index(key->data, key->len, &element)) { + ScrDyn *view = scr_dyn_typed_ref_materialize(recv); + if (!scr_exc_pending() && view->kind == SCR_DYN_ARR) { + bool own = view->v.arr.properties && scr_dyn_obj_get(view->v.arr.properties, key->data, key->len); + if (own) scr_dyn_key_delete(view, key, strict); + if (own && !scr_exc_pending()) scr_dyn_typed_ref_commit(recv); + scr_dyn_release(view); + return; + } + scr_dyn_release(view); + if (scr_exc_pending()) return; + } if (recv->kind == SCR_DYN_FUNC) { if (scr_dyn_fn_property_fence(key)) return; ScrDyn *table = scr_dyn_fn_properties(recv); diff --git a/tests/corpus/6987-array-view-named-properties.ts b/tests/corpus/6987-array-view-named-properties.ts new file mode 100644 index 0000000000..5cbea5a0ac --- /dev/null +++ b/tests/corpus/6987-array-view-named-properties.ts @@ -0,0 +1,79 @@ +// Named properties written to a typed array through an `unknown` view stay +// on the array: later views read them, key walks list them after the +// indices, and `for...in` over the typed array visits them. Holes are +// skipped by `for...in`. + +type Bag = Record; + +const numbers: number[] = [1, 2, 3]; +const numbersView = numbers as unknown as Bag; +numbersView.extra = "named"; +console.log(numbersView.extra, (numbers as unknown as Bag).extra); +console.log(Object.keys(numbersView), JSON.stringify(numbers), numbers.length); +console.log("extra" in numbersView, Object.hasOwn(numbersView, "extra")); + +numbers.push(4); +numbers[0] = 10; +console.log((numbers as unknown as Bag).extra, numbers.join(",")); +console.log((numbers.slice() as unknown as Bag).extra); + +(numbers as unknown as Bag).extra = "again"; +(numbers as unknown as Bag).count = 2; +console.log(Object.entries(numbers as unknown as Bag)); +for (const key in numbers) console.log("numbers key", key); + +delete (numbers as unknown as Bag).extra; +console.log((numbers as unknown as Bag).extra, Object.keys(numbers as unknown as Bag)); + +const words: string[] = ["a", "b"]; +(words as unknown as Bag).label = { nested: [1, 2] }; +const label = (words as unknown as Bag).label as { nested: number[] }; +console.log(label.nested.length, words.length); +for (const key in words) console.log("words key", key); + +interface Point { + x: number; + y: number; +} +const points: Point[] = [{ x: 1, y: 2 }]; +(points as unknown as Bag).origin = "zero"; +console.log((points as unknown as Bag).origin, points[0]!.x); +for (const key in points) console.log("points key", key); + +class Item { + name: string; + constructor(name: string) { + this.name = name; + } +} +const items: Item[] = [new Item("first")]; +(items as unknown as Bag).kind = "items"; +console.log((items as unknown as Bag).kind, items[0]!.name); + +const flags: boolean[] = [true, false]; +Object.defineProperty(flags as unknown as Bag, "hidden", { value: 1, enumerable: false }); +(flags as unknown as Bag).shown = 2; +console.log((flags as unknown as Bag).hidden, (flags as unknown as Bag).shown); +for (const key in flags) console.log("flags key", key); + +const holes: (number | undefined)[] = [1, , 3, undefined]; +for (const key in holes) console.log("holes key", key); +(holes as unknown as Bag).tail = true; +for (const key in holes) console.log("holes key after", key); + +const offsets: number[] = [1, 2]; +offsets[-1] = 5; +offsets[1.5] = 6; +(offsets as unknown as Bag).name = "offsets"; +console.log(offsets[-1], offsets[1.5], (offsets as unknown as Bag).name); +for (const key in offsets) console.log("offsets key", key); + +const shrinking: number[] = [1, 2, 3, 4]; +(shrinking as unknown as Bag).after = "kept"; +for (const key in shrinking) { + console.log("shrinking key", key); + if (key === "1") shrinking.length = 2; +} + +const match = /b(?c)?/.exec("abc"); +if (match) for (const key in match) console.log("match key", key); diff --git a/tests/harness/corpus-costs/6987-array-view-named-properties.ts.json b/tests/harness/corpus-costs/6987-array-view-named-properties.ts.json new file mode 100644 index 0000000000..1e9e64ec59 --- /dev/null +++ b/tests/harness/corpus-costs/6987-array-view-named-properties.ts.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 4400 } From d15e822d263203f3bc29cb43071b47d6f21be839 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 17:16:12 -0500 Subject: [PATCH 010/395] Hold any non-nullish value in an empty object type inferred from a literal The type mapper lowered the type tsc infers from an empty object literal (`const o = {}`) as the empty record, while a declared `{}` already lowered as a checked-dynamic value. tsc's `{}` admits every non-nullish value in both cases: union subtype reduction folds primitives into a literal's `{}` (`["12", {}]` is `{}[]`, `flag ? "x" : {}` is `{}`), and `let o = {}` accepts `o = "x"`. Such a program built and then threw a TypeError at run time ("expected object at $[0], got string" in a JavaScript file, "a 'string' value is not representable in the target union" in TypeScript), where Node prints the values. An empty object type with no members, index signatures or signatures now lowers as a checked-dynamic value, the same rule as `object` and `Object`. As an arm of a union that keeps another non-nullish arm, which subtype reduction would have removed, it holds only the literal's objects and stays the empty record. An assertion's scalar check (`as string`, `as number`, `as boolean`) on a dynamic value whose result goes straight back into a dynamic slot keeps the original value, since JavaScript erases assertions: passing `{} as unknown as string` to process.loadEnvFile reports Node's ERR_INVALID_ARG_TYPE instead of a TypeError from the check. Object.assign's two-argument path for a dynamic target now applies the same refusal as the general path when a class instance is copied into a fresh object literal whose result the checker types as the class, so `Object.assign({}, instance)` keeps its compile-time refusal now that the literal lowers as a dynamic value. --- .../src/frontend/lowering/lower-calls.ts | 45 ++++++----- .../compiler/src/frontend/lowering/lowerer.ts | 14 ++++ packages/compiler/src/frontend/type-mapper.ts | 77 +++++++++++++++---- .../6909-empty-object-type-values.ts.json | 6 ++ .../6910-empty-object-type-values-js.mjs.json | 6 ++ tests/corpus/6909-empty-object-type-values.ts | 32 ++++++++ .../6910-empty-object-type-values-js.mjs | 14 ++++ .../index-signature-values.ts.txt | 20 +---- ...ine-2104-computed-key-destructuring.ts.txt | 11 +-- .../__snapshots__/utility-types.ts.txt | 20 +---- 10 files changed, 164 insertions(+), 81 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6909-empty-object-type-values.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6910-empty-object-type-values-js.mjs.json create mode 100644 tests/corpus/6909-empty-object-type-values.ts create mode 100644 tests/corpus/6910-empty-object-type-values-js.mjs diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index ac77288380..fa90990e50 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -7668,6 +7668,10 @@ function lowerObjectStaticCall( const target = lowerer.lowerExpr(call.arguments.nodes[0]!); const rawSource = lowerer.lowerExpr(call.arguments.nodes[1]!); fenceSymbolFieldCopy(lowerer, call.arguments.nodes[1]!, rawSource.type); + let targetNode: ts.Expression = call.arguments.nodes[0]!; + while (ts.isParenthesizedExpression(targetNode)) targetNode = targetNode.expression; + if (ts.isObjectLiteralExpression(targetNode)) + fenceClassCopyIntoFreshObject(lowerer, call, call.arguments.nodes[1]!, rawSource); const source = lowerer.coerceToExpected(rawSource, DYN); if (target.type.kind === "dyn" && source.type.kind === "dyn") { return { kind: "libCall", fn: "dyn.assign", args: [target, source], type: DYN, loc }; @@ -7821,25 +7825,8 @@ function lowerObjectStaticCall( const srcNode = spread ? argNode.expression : argNode; const rawSource = lowerer.lowerExpr(srcNode); fenceSymbolFieldCopy(lowerer, srcNode, rawSource.type, spread); - // A class instance copied into a fresh object - // (`Object.assign({}, instance)`): the result is a plain - // object holding the instance's own fields, never an - // instance, so a result the checker types as the class has - // no representation (the checked cast would throw). - if ( - rawSource.type.kind === "object" && - freshLiteralTarget && - !ts.isExpressionStatement(call.parent) - ) { - const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); - if (resultT !== null && resultT.kind === "object") { - lowerer.unsupported( - "SC1090", - srcNode, - `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)`, - ); - } - } + if (freshLiteralTarget) + fenceClassCopyIntoFreshObject(lowerer, call, srcNode, rawSource); const src = lowerer.coerceToExpected(rawSource, DYN); if (src.type.kind !== "dyn") { ok = false; @@ -10685,3 +10672,23 @@ function functionIntegrity( loc, }; } + +/** A class instance copied into a fresh object (`Object.assign({}, + * instance)`): the result is a plain object holding the instance's own + * fields, never an instance, so a result the checker types as the class has + * no representation (the checked cast would throw). */ +function fenceClassCopyIntoFreshObject( + lowerer: Lowerer, + call: ts.CallExpression, + srcNode: ts.Expression, + rawSource: IrExpr, +): void { + if (rawSource.type.kind !== "object" || ts.isExpressionStatement(call.parent)) return; + const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); + if (resultT === null || resultT.kind !== "object") return; + lowerer.unsupported( + "SC1090", + srcNode, + `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)`, + ); +} diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index c4d24cef17..e287bac3f0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -9786,6 +9786,20 @@ export class Lowerer { * directly when the expression was already lowered. */ coerceInto(node: ts.Node, expr: IrExpr, expected: IrType): IrExpr { refuseInferredCallbackDrops(this, node, expr.type, expected); + // An assertion's scalar check whose value goes straight back into a + // checked-dynamic slot (`process.loadEnvFile({} as unknown as string)`) + // keeps the original value: JavaScript erases assertions, so the check + // could only add a TypeError Node does not throw, and the slot holds + // the value either way. + if (expected.kind === "dyn" && expr.kind === "dynCheck" && expr.value.type.kind === "dyn") { + let source = node; + while (ts.isParenthesizedExpression(source)) source = source.expression; + if ( + (ts.isAsExpression(source) || ts.isTypeAssertion(source)) && + (expr.type.kind === "string" || expr.type.kind === "f64" || expr.type.kind === "bool") + ) + return expr.value; + } // A union destination can accept the binding's tagged storage directly. // The checker's single-arm flow type can be stale after a callback writes // the binding. Extracting that arm and wrapping it again would discard the diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 3448545f8c..6305a6b63a 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1108,6 +1108,11 @@ export type TypeParamTsResolver = (t: ts.Type) => ts.Type | null; * once per Lowerer and threaded through the recursion — adding a hook means * one new field here, not a new parameter at every call site. */ export interface TypeMapperCtx { + /** Set only while a union maps an arm that is the type inferred from an + * empty object literal, beside another non-nullish arm: that arm holds + * only the literal's objects, so it maps to the empty record (see the + * empty object type rule in the object mapping). */ + emptyLiteralArm?: boolean; checker: ts.TypeChecker; /** JavaScript declarations whose instance or object-literal types only * the checked-dynamic representation holds (js-dynamic-objects.ts). @@ -4648,6 +4653,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ); }; const hasArrayPart = parts.some((part) => checker.isArrayType(part) || matchArray(part)); + // Subtype reduction removes every arm assignable to `{}`, so a union + // that keeps another non-nullish arm beside an empty literal's `{}` + // was not reduced: that `{}` arm holds only the literal's objects. + const nullish = ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void; + const literalEmptyArms = + parts.some((part) => isEmptyObjectLiteralType(part, ctx)) && + parts.some( + (part) => + (part.flags & nullish) === 0 && + !checker.isNeverType(part) && + !isEmptyObjectLiteralType(part, ctx), + ); for (const part of parts) { if (hasArrayPart && emptyTuple(part)) continue; // TypeScript's client can retain impossible intersections in a @@ -4666,7 +4683,17 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { byKey.set(typeKey(NULL_T), NULL_T); continue; } - const mapped = mapType(part, ctx); + let mapped: IrType | null; + if (literalEmptyArms && isEmptyObjectLiteralType(part, ctx)) { + ctx.emptyLiteralArm = true; + try { + mapped = mapType(part, ctx); + } finally { + ctx.emptyLiteralArm = false; + } + } else { + mapped = mapType(part, ctx); + } if (!mapped) { return null; @@ -7496,25 +7523,29 @@ function mapRecordTypeInner( // An INDEX-SIGNATURE shape is exempt: `Record` legitimately // has zero declared members — the signature is the shape. if (computed && props.length === 0 && !indexValue) return null; - // A DECLARED empty object type — `{}` (spelled or the checker's shared - // intrinsic), `interface Empty {}` — is tsc's TOP type over non-nullish - // values: every number, string, record, array, function, or class - // instance is assignable into the slot, so the exact-record stance - // cannot hold it. It lowers like `unknown` (the dyn), the same rule - // as `object` and the lib's `Object`: assignments convert at the site - // (sources outside the checked-dynamic tree fence there, exactly as unknown does), reads - // narrow back out through the same checked casts. The type INFERRED - // from an empty object literal (`const o = {}`) stays the empty record - // — it describes a value the program built, not an annotation that - // admits everything — and computed empties (`Partial` in a generic - // body) returned null above. Empty shapes WITH an index signature - // (`Record`) are not empty: the signature is the shape. + // An empty object type — `{}` (spelled, the checker's shared + // intrinsic, or inferred from an empty object literal), `interface + // Empty {}` — is tsc's TOP type over non-nullish values: every number, + // string, record, array, function, or class instance is assignable + // into the slot, so the exact-record stance cannot hold it. It lowers + // like `unknown` (the dyn), the same rule as `object` and the lib's + // `Object`: assignments convert at the site (sources outside the + // checked-dynamic tree fence there, exactly as unknown does), reads + // narrow back out through the same checked casts. A literal's inferred + // `{}` is no exception: union subtype reduction folds primitives into + // it (`["12", {}]` is `{}[]`, `flag ? "x" : {}` is `{}`), and `let o = + // {}` admits `o = "x"`. As an arm beside another non-nullish arm (the + // union was not reduced, `string | {} | DataView`) it holds only the + // literal's objects and stays the empty record (emptyLiteralArm). + // Computed empties (`Partial` in a generic body) returned null + // above. Empty shapes WITH an index signature (`Record`) are + // not empty: the signature is the shape. if ( props.length === 0 && indexValue === undefined && checker.getCallSignatures(widened).length === 0 && checker.getConstructSignatures(widened).length === 0 && - !(anonSym !== undefined && anonSym.declarations.some((d) => ts.isObjectLiteralExpression(d))) + !(ctx.emptyLiteralArm && isEmptyObjectLiteralType(widened, ctx)) ) { return DYN; } @@ -8154,3 +8185,19 @@ function innerBlockedMember( } return here; } + +/** The type tsc infers from an empty object literal (`{}` written as a + * value): no members, index signatures or signatures, declared by an object + * literal expression. */ +function isEmptyObjectLiteralType(type: ts.Type, ctx: TypeMapperCtx): boolean { + const { checker } = ctx; + const symbol = type.getSymbol(); + return ( + symbol !== undefined && + symbol.declarations.some((d) => ts.isObjectLiteralExpression(d)) && + checker.getPropertiesOfType(type).length === 0 && + checker.getIndexInfosOfType(type).length === 0 && + checker.getCallSignatures(type).length === 0 && + checker.getConstructSignatures(type).length === 0 + ); +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6909-empty-object-type-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6909-empty-object-type-values.ts.json new file mode 100644 index 0000000000..e94a525ad2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6909-empty-object-type-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6909-empty-object-type-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6910-empty-object-type-values-js.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6910-empty-object-type-values-js.mjs.json new file mode 100644 index 0000000000..1e07899f1c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6910-empty-object-type-values-js.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6910-empty-object-type-values-js.mjs" + ], + "diags": [] +} diff --git a/tests/corpus/6909-empty-object-type-values.ts b/tests/corpus/6909-empty-object-type-values.ts new file mode 100644 index 0000000000..b539415fde --- /dev/null +++ b/tests/corpus/6909-empty-object-type-values.ts @@ -0,0 +1,32 @@ +// The empty object type `{}` admits every non-nullish value, including when +// tsc infers it from an empty object literal: union subtype reduction folds +// primitives into it (`["12", {}]` is `{}[]`, `flag ? "x" : {}` is `{}`), and +// a `let` initialized with `{}` takes any later non-nullish value. +const flag = Math.random() < 2; + +const mixed = ["12", {}]; +console.log(mixed.length, typeof mixed[0], typeof mixed[1]); + +const marker = {}; +const list = [marker, "x", 3, true, [1]]; +console.log(list.length, list[0] === marker, list.map((x) => typeof x).join(",")); + +const holder = { value: flag ? "x" : {} }; +const picked = flag ? "y" : {}; +console.log(typeof holder.value, typeof picked, holder.value, picked); + +let slot = {}; +console.log(JSON.stringify(slot)); +slot = "now a string"; +console.log(typeof slot, slot); +slot = 42; +console.log(typeof slot, slot); + +function choose(n: number) { + if (n > 0) return "positive"; + return {}; +} +console.log(typeof choose(1), typeof choose(-1)); + +const empty = {}; +console.log(JSON.stringify(empty), Object.keys(empty).length, empty === empty, String(empty)); diff --git a/tests/corpus/6910-empty-object-type-values-js.mjs b/tests/corpus/6910-empty-object-type-values-js.mjs new file mode 100644 index 0000000000..3c68035564 --- /dev/null +++ b/tests/corpus/6910-empty-object-type-values-js.mjs @@ -0,0 +1,14 @@ +// In a JavaScript file, an array literal mixing strings, other primitives and +// an empty object holds each value as written. +const a = ["12", {}]; +console.log(a.length, typeof a[0], typeof a[1]); + +const b = ["12", "", true, null, undefined, [7], {}]; +console.log(b.length, b.map((x) => (x === null ? "null" : typeof x)).join(",")); + +const options = [{}, "fallback"]; +for (const option of options) console.log(typeof option === "string" ? option : JSON.stringify(option)); + +let current = {}; +current = "replaced"; +console.log(current); diff --git a/tests/harness/__snapshots__/index-signature-values.ts.txt b/tests/harness/__snapshots__/index-signature-values.ts.txt index d1e9395ed4..bbbed7fe2a 100644 --- a/tests/harness/__snapshots__/index-signature-values.ts.txt +++ b/tests/harness/__snapshots__/index-signature-values.ts.txt @@ -7,15 +7,6 @@ index-signature-values.ts:8:7 - error SC2006: values of type '{ [s: symbol]: num hint: string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering -index-signature-values.ts:9:13 - error SC2004: uses of 'bySymbol' inherit the blocker on its declaration - - 8 | const bySymbol: { [s: symbol]: number } = {}; - 9 | console.log(bySymbol); - | ^~~~~~~~ - 10 | - - hint: the declaration of 'bySymbol' did not compile — fix the diagnostic reported there and these sites clear with it - index-signature-values.ts:13:13 - error SC1090: console.log of '{ [key: string]: Uint8Array }' values (Uint8Array and Buffer values inside composites have no inspect lowering yet (Buffer's form vs Uint8Array's is not recorded in the static type here)) are not supported yet 12 | const buffers: Record = {}; @@ -30,13 +21,4 @@ index-signature-values.ts:18:7 - error SC2006: values of type '{ [k: string]: st | ^~~~ 19 | console.log(dual); - hint: string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering - -index-signature-values.ts:19:13 - error SC2004: uses of 'dual' inherit the blocker on its declaration - - 18 | const dual: { [k: string]: string | number; [n: number]: number } = {}; - 19 | console.log(dual); - | ^~~~ - 20 | - - hint: the declaration of 'dual' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file + hint: string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering \ No newline at end of file diff --git a/tests/harness/__snapshots__/retired-engine-2104-computed-key-destructuring.ts.txt b/tests/harness/__snapshots__/retired-engine-2104-computed-key-destructuring.ts.txt index bb30cf5819..b5417ae72c 100644 --- a/tests/harness/__snapshots__/retired-engine-2104-computed-key-destructuring.ts.txt +++ b/tests/harness/__snapshots__/retired-engine-2104-computed-key-destructuring.ts.txt @@ -7,18 +7,11 @@ retired-engine-2104-computed-key-destructuring.ts:30:38 - error SC2009: values o hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') -retired-engine-2104-computed-key-destructuring.ts:33:8 - error SC1031: destructuring with computed keys that do not fold to one property name is not supported yet - - 32 | traceS = ""; - 33 | let [{ [order(1)]: y } = order(0)] = [{}]; - | ^~~~~~~~~~~~~ - 34 | console.log(traceS); - -retired-engine-2104-computed-key-destructuring.ts:36:7 - error SC1031: destructuring with computed keys that do not fold to one property name is not supported yet +retired-engine-2104-computed-key-destructuring.ts:36:58 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet 35 | traceS = ""; 36 | let { [order(0)]: { [order(2)]: z } = order(1), ...w } = {} as any; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + | ^~~~~~~~~ 37 | console.log(traceS); retired-engine-2104-computed-key-destructuring.ts:39:8 - error SC1031: rest bindings whose packed type has no static mapping (only plain record types pack) is not supported yet diff --git a/tests/harness/__snapshots__/utility-types.ts.txt b/tests/harness/__snapshots__/utility-types.ts.txt index 71f4d3083e..02595967b2 100644 --- a/tests/harness/__snapshots__/utility-types.ts.txt +++ b/tests/harness/__snapshots__/utility-types.ts.txt @@ -7,15 +7,6 @@ utility-types.ts:15:7 - error SC2006: values of type '{ [s: symbol]: string; }' hint: string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering -utility-types.ts:16:13 - error SC2004: uses of 'bySymbol' inherit the blocker on its declaration - - 15 | const bySymbol: { [s: symbol]: string } = {}; - 16 | console.log(bySymbol); - | ^~~~~~~~ - 17 | - - hint: the declaration of 'bySymbol' did not compile — fix the diagnostic reported there and these sites clear with it - utility-types.ts:18:7 - error SC2006: values of type '{ [k: string]: Date; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown') 17 | @@ -23,13 +14,4 @@ utility-types.ts:18:7 - error SC2006: values of type '{ [k: string]: Date; }' ca | ^~~~~~~ 19 | const boot = dateBag["boot"]; - hint: string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering - -utility-types.ts:19:14 - error SC2004: uses of 'dateBag' inherit the blocker on its declaration - - 18 | const dateBag: { [k: string]: Date } = {}; - 19 | const boot = dateBag["boot"]; - | ^~~~~~~ - 20 | console.log(boot); - - hint: the declaration of 'dateBag' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file + hint: string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering \ No newline at end of file From 4fee347ca0baf7cdcc4c0ac299865ea514e39e71 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 17:57:00 -0500 Subject: [PATCH 011/395] Address review feedback on array named properties --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/backend/llvm/reference-writes.ts | 1 + .../src/frontend/lowering/lower-stmts.ts | 26 ++++++++++------ .../compiler/src/ir/builtin-signatures.ts | 3 ++ packages/compiler/src/ir/validate.ts | 4 +-- packages/runtime/src/scr_array.c | 31 +++++++++++++++++++ packages/runtime/src/scr_array.h | 7 +++++ packages/runtime/src/scr_json.c | 12 +++++-- .../6987-array-view-named-properties.ts | 17 ++++++++++ 9 files changed, 89 insertions(+), 13 deletions(-) diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index be7a4973b1..b7c6b8a219 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1520,6 +1520,7 @@ export const LIB_FN_SYMS: Record = { "regex.groupParseInt": "scr_regex_group_parse_int", "regex.resultProp": "scr_regex_result_prop", "array.namedKeys": "scr_arr_named_keys", + "array.hasNamedKey": "scr_arr_has_named_key", // queueMicrotask's checked-dynamic form (borrowed dyn; a non-function // throws synchronously), the minted setImmediate value, and the // timers/promises immediate — all mark the loop live. diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index c77a5a191c..d31f14d12b 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -106,6 +106,7 @@ const INERT_LIB_CALLS = new Set([ "regex.groupParseInt", "regex.resultProp", "array.namedKeys", + "array.hasNamedKey", "process.cwd", "process.argv", "process.execArgv", diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 0960b9d6aa..78f2f4189e 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -11620,21 +11620,23 @@ function lowerForInArray( loc, }); // Indices first, then the named keys the array had when the loop began. + const namedKey = (): IrExpr => ({ + kind: "arrayGet", + arr: keysRef(), + index: { kind: "bin", op: "-", left: iRef(), right: lenRef(), type: F64, loc }, + type: STRING, + loc, + }); const keyInit: IrExpr = { kind: "ternary", cond: isIndex(), then: { kind: "toString", operand: iRef(), type: STRING, loc }, - else_: { - kind: "arrayGet", - arr: keysRef(), - index: { kind: "bin", op: "-", left: iRef(), right: lenRef(), type: F64, loc }, - type: STRING, - loc, - }, + else_: namedKey(), type: STRING, loc, }; - // An index is visited while it is below the live length and not a hole. + // An index is visited while it is below the live length and not a hole, + // a named key while the array still has it. const visits: IrExpr = { kind: "ternary", cond: isIndex(), @@ -11653,7 +11655,13 @@ function lowerForInArray( type: BOOL, loc, }, - else_: { kind: "boolLit", value: true, type: BOOL, loc }, + else_: { + kind: "libCall", + fn: "array.hasNamedKey", + args: [arrRef(), namedKey()], + type: BOOL, + loc, + }, type: BOOL, loc, }; diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 1e62703660..e4bd83db72 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3727,6 +3727,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ // The string keys `for...in` visits on an array (args[0], any element // type) after its indices. Borrowed; never throws. "array.namedKeys": { argTypes: [null], result: arrayOf(STRING) }, + // 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 }, // 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 0004be761c..88ccbbc339 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4262,9 +4262,9 @@ function validateFunction( if (!ok) err(`libCall dyn.objEntryPairs must return a [string, any] tuple array`, e.loc); return true; } - if (e.fn === "array.namedKeys") { + if (e.fn === "array.namedKeys" || e.fn === "array.hasNamedKey") { if (e.args[0]?.type.kind !== "array") { - err(`libCall array.namedKeys arg 0: expected an array, got ${e.args[0]?.type.kind}`, e.loc); + err(`libCall ${e.fn} arg 0: expected an array, got ${e.args[0]?.type.kind}`, e.loc); } return true; } diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index fa98dd38a3..c64b859478 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -4790,6 +4790,22 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs) { return out; } +bool scr_arr_numeric_key(const char *key, size_t len, double *out) { + char text[64]; + if (len == 0 || len >= sizeof text) return false; + memcpy(text, key, len); + text[len] = '\0'; + char *end; + double number = strtod(text, &end); + if (end != text + len) return false; + char spelled[64]; + if (scr_f64_to_str(number, spelled) != len || memcmp(spelled, key, len) != 0) return false; + size_t index; + if (scr_arr_valid_index(number, &index)) return false; + *out = number; + return true; +} + 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 && @@ -4838,6 +4854,21 @@ ScrArr *scr_arr_named_keys(ScrArr *a) { return keys; } +bool scr_arr_has_named_key(ScrArr *a, ScrStr *key) { + const ScrArrExt *x = scr_arr_ext_of(a); + if (!x) return false; + if (scr_arr_prop_index(a, key->data, key->len) != SIZE_MAX) return true; + const ScrDyn *metadata = x->metadata; + if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE) + return true; /* index, input and groups, the only keys a pending result lists */ + if (!metadata || metadata->kind != SCR_DYN_OBJ) return false; + for (size_t i = 0; i < metadata->v.obj.len; i++) { + const ScrDynEntry *entry = &metadata->v.obj.entries[i]; + if (entry->key_len == key->len && memcmp(entry->key, key->data, key->len) == 0) return true; + } + return false; +} + void scr_arr_adopt_view_properties(ScrArr *a, ScrDyn *view, ScrArr *target) { ScrArrExt *from = scr_arr_ext_of(target); if (from && from->prop_len) { diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index d9f5f49bc1..6c41b3dbd8 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -322,6 +322,13 @@ void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target); * (non-index) properties as its metadata and the noncanonical numeric * properties of `target`, so neither is lost when the storage is swapped. */ void scr_arr_adopt_view_properties(ScrArr *a, struct ScrDyn *view, ScrArr *target); +/* True when `key` is the string spelling of a number that is not an array + * index ("-1", "1.5", "NaN", "Infinity"), with the number in *out. Arrays + * keep such properties in their noncanonical property table. */ +bool scr_arr_numeric_key(const char *key, size_t len, double *out); +/* Whether the array still has the named or noncanonical numeric property + * `key`: `for...in` skips a key deleted before its turn. Borrows both. */ +bool scr_arr_has_named_key(ScrArr *a, ScrStr *key); /* The enumerable string keys `for...in` visits after the indices: the * noncanonical numeric properties, then the named properties in insertion * order. +1. */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 8577a96fb8..5d00f8b479 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -10270,9 +10270,17 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) { return; } /* A live native array keeps its named properties in its metadata, so - * deleting one goes through the view and its commit. Elements and - * `length` keep the boundary below. */ + * deleting one goes through the view and its commit. Numeric properties + * are deleted natively. Elements and `length` keep the boundary below. */ size_t element; + double number; + if (recv->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(recv) && + scr_arr_numeric_key(key->data, key->len, &number)) { + /* Noncanonical numeric properties ("-1", "1.5") live in the native + * array's property table. */ + scr_arr_delete(recv->v.typed_ref.ptr, number); + return; + } if (recv->kind == SCR_DYN_TYPED_REF && recv->v.typed_ref.commit && scr_dyn_typed_ref_array_ops(recv) && !scr_typed_ref_is_length(key) && !scr_typed_ref_canonical_index(key->data, key->len, &element)) { ScrDyn *view = scr_dyn_typed_ref_materialize(recv); diff --git a/tests/corpus/6987-array-view-named-properties.ts b/tests/corpus/6987-array-view-named-properties.ts index 5cbea5a0ac..bd100b8b1e 100644 --- a/tests/corpus/6987-array-view-named-properties.ts +++ b/tests/corpus/6987-array-view-named-properties.ts @@ -77,3 +77,20 @@ for (const key in shrinking) { const match = /b(?c)?/.exec("abc"); if (match) for (const key in match) console.log("match key", key); + +// A numeric property deleted through a view is gone from the array. +const numeric: number[] = [1, 2]; +numeric[-1] = 5; +delete (numeric as unknown as Bag)["-1"]; +console.log(numeric[-1], "-1" in (numeric as unknown as Bag)); +for (const key in numeric) console.log("numeric key", key); + +// A named key deleted before its turn is not visited. +const visited: number[] = [1]; +(visited as unknown as Bag).first = 1; +(visited as unknown as Bag).second = 2; +(visited as unknown as Bag).third = 3; +for (const key in visited) { + console.log("visited key", key); + if (key === "first") delete (visited as unknown as Bag).second; +} From 982ef6dbefe5beb5c98ae64065a0b96dd94c4e01 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 18:11:33 -0500 Subject: [PATCH 012/395] Allow zlib callbacks to assign to their result The error-path copy of a zlib callback reads its result from a hidden local that was immutable, so a body that assigned a fallback Buffer to the result (if (!out) out = Buffer.from("fallback")) failed to compile with an internal compiler error. The local is now mutable, and an assigned value is read back as a checked-dynamic value, as in Node. --- .../src/frontend/lowering/lower-functions.ts | 4 +++- tests/corpus/6920-zlib-callback-error-result.ts | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 6fc083468f..8f2c575a4d 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -2,6 +2,7 @@ import { declareTypedArguments } from "./typed-arguments.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; import { bindingNeverReassigned } from "./binding-analysis.js"; +import { declareContextLocal } from "./function-context.js"; import { isErrorFirstErrorType } from "./builtins/arguments.js"; import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; @@ -786,7 +787,8 @@ function lowerUndefinedResultBody( lowerer.suppressStats = stats; } }; - const slot = lowerer.declareHiddenLocal("%result", DYN); + // Mutable: the body may assign to its result (`if (!out) out = ...`). + const slot = declareContextLocal(lowerer.ctx, "%result", DYN, true, undefined, undefined); const undefinedResult: IrStmt = { kind: "varDecl", localId: slot.id, diff --git a/tests/corpus/6920-zlib-callback-error-result.ts b/tests/corpus/6920-zlib-callback-error-result.ts index cc91282213..5016fa8418 100644 --- a/tests/corpus/6920-zlib-callback-error-result.ts +++ b/tests/corpus/6920-zlib-callback-error-result.ts @@ -176,6 +176,19 @@ function namedRetry(): void { }); } +function fallbackResult(): void { + // The body assigns a fallback Buffer to its result. + zlib.inflate(bad, (err, out) => { + if (!out) out = Buffer.from("fallback"); + console.log("fallback", err?.code, typeof out, out.toString()); + zlib.inflate(good, (err2, out2) => { + if (err2) out2 = Buffer.alloc(0); + console.log("fallback nested", err2, out2.length); + step(); + }); + }); +} + function functionValues(next: () => void): void { steps.push( () => zlib.inflate(good, named), @@ -203,6 +216,7 @@ function functionValues(next: () => void): void { () => zlib.inflate(good, captures), nestedForward, namedRetry, + fallbackResult, () => { named(new Error("direct"), Buffer.from("both")); }, From 34f371f8295d1b5ffd664bc21355aeab4b886b6c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 18:43:03 -0500 Subject: [PATCH 013/395] Address the second review round on array named properties --- packages/runtime/src/scr_json.c | 38 +++++++++---------- .../6987-array-view-named-properties.ts | 8 ++++ 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 5d00f8b479..f62ef01655 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -10269,30 +10269,30 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) { } return; } - /* A live native array keeps its named properties in its metadata, so - * deleting one goes through the view and its commit. Numeric properties - * are deleted natively. Elements and `length` keep the boundary below. */ + /* A live native array keeps the noncanonical numeric properties written + * natively ("-1", "1.5") in its property table, and named properties, + * including numeric-looking keys written through a view, in its metadata, + * which the view and its commit hold. Deleting such a key removes it from + * both. Elements and `length` keep the boundary below. */ size_t element; - double number; if (recv->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(recv) && - scr_arr_numeric_key(key->data, key->len, &number)) { - /* Noncanonical numeric properties ("-1", "1.5") live in the native - * array's property table. */ - scr_arr_delete(recv->v.typed_ref.ptr, number); - return; - } - if (recv->kind == SCR_DYN_TYPED_REF && recv->v.typed_ref.commit && scr_dyn_typed_ref_array_ops(recv) && !scr_typed_ref_is_length(key) && !scr_typed_ref_canonical_index(key->data, key->len, &element)) { - ScrDyn *view = scr_dyn_typed_ref_materialize(recv); - if (!scr_exc_pending() && view->kind == SCR_DYN_ARR) { - bool own = view->v.arr.properties && scr_dyn_obj_get(view->v.arr.properties, key->data, key->len); - if (own) scr_dyn_key_delete(view, key, strict); - if (own && !scr_exc_pending()) scr_dyn_typed_ref_commit(recv); + double number; + bool numeric = scr_arr_numeric_key(key->data, key->len, &number); + if (numeric) scr_arr_delete(recv->v.typed_ref.ptr, number); + if (recv->v.typed_ref.commit) { + ScrDyn *view = scr_dyn_typed_ref_materialize(recv); + if (!scr_exc_pending() && view->kind == SCR_DYN_ARR) { + bool own = view->v.arr.properties && scr_dyn_obj_get(view->v.arr.properties, key->data, key->len); + if (own) scr_dyn_key_delete(view, key, strict); + if (own && !scr_exc_pending()) scr_dyn_typed_ref_commit(recv); + scr_dyn_release(view); + return; + } scr_dyn_release(view); - return; + if (scr_exc_pending()) return; } - scr_dyn_release(view); - if (scr_exc_pending()) return; + if (numeric) return; } if (recv->kind == SCR_DYN_FUNC) { if (scr_dyn_fn_property_fence(key)) return; diff --git a/tests/corpus/6987-array-view-named-properties.ts b/tests/corpus/6987-array-view-named-properties.ts index bd100b8b1e..faba7dc90e 100644 --- a/tests/corpus/6987-array-view-named-properties.ts +++ b/tests/corpus/6987-array-view-named-properties.ts @@ -85,6 +85,14 @@ delete (numeric as unknown as Bag)["-1"]; console.log(numeric[-1], "-1" in (numeric as unknown as Bag)); for (const key in numeric) console.log("numeric key", key); +// A numeric-looking key written through a view is deleted through a view too. +const viewNumeric: string[] = ["a"]; +(viewNumeric as unknown as Bag)["-2"] = "view"; +console.log((viewNumeric as unknown as Bag)["-2"], "-2" in (viewNumeric as unknown as Bag)); +delete (viewNumeric as unknown as Bag)["-2"]; +console.log((viewNumeric as unknown as Bag)["-2"], "-2" in (viewNumeric as unknown as Bag)); +for (const key in viewNumeric) console.log("view numeric key", key); + // A named key deleted before its turn is not visited. const visited: number[] = [1]; (visited as unknown as Bag).first = 1; From fa7bc7c7a0d73e55de132ea929b03257d7e51fd0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 18:53:43 -0500 Subject: [PATCH 014/395] Warn about likely-hot functions that use untyped values A build now names the functions of the program's own files that compile to the dynamic tier and likely run often, so a slow program with a little untyped code says where the time goes: warning SC6101: 3 functions that likely run often use untyped values, which runs slower than typed code: advance (nbody.js:22): untyped JavaScript file, in a loop ... - The functions come from the tier assignment lowering already makes (Lowerer.dynamicFunctions). Without a profile, a function counts as likely hot when a dynamic operation runs inside one of its loops, when a loop calls it, or when it calls itself (lowering/dynamic-tier.ts). - A function is named only when its source says why its values are untyped: an `any` (for a for...of variable, the value it iterates), an untyped JSON.parse result, or an untyped JavaScript file. Generated %dyn.* helpers, module top levels and functions that are dynamic for an operation without a static lowering are left out. - At most five functions are named, most likely hot first, with the total; dynamic-tier code in npm packages is counted per package on one line. All of the warning's text is in diagnostics/dynamic-tier.ts. - SC6101 opens the SC61xx band, performance notes, with a new "performance" category in the diagnostics envelope; SC60xx stay the divergences. - scriptc build prints it after the divergence warnings, and the executable cache stores it with the entry, so a cached build (including the CLI's routed fast path) repeats it. scriptc coverage reports it, and a comment-only edit no longer replays a verdict that carries it, since its text holds line numbers. - --no-dynamic-tier-warning leaves it out of build and coverage; coverage --fail-on=dynamic fails on it. - Docs: coverage.mdx (Dynamic tier, Gating builds and CI, the envelope categories) and cli.mdx. --- .gitignore | 1 + docs/content/docs/cli.mdx | 7 +- docs/content/docs/coverage.mdx | 23 +- packages/cli/src/bootstrap.ts | 5 + .../cli/test/dynamic-tier-warning.test.ts | 120 ++++++++ packages/compiler/src/cli/command.ts | 40 ++- packages/compiler/src/cli/usage.ts | 9 +- packages/compiler/src/coverage/report.ts | 24 +- .../compiler/src/coverage/verdict-cache.ts | 3 + .../compiler/src/coverage/verdict-semantic.ts | 3 + .../compiler/src/diagnostics/diagnostic.ts | 5 +- .../compiler/src/diagnostics/dynamic-tier.ts | 99 ++++++ packages/compiler/src/diagnostics/envelope.ts | 8 +- .../src/executable/executable-cache.ts | 11 + packages/compiler/src/executable/prepare.ts | 2 +- packages/compiler/src/frontend/analysis.ts | 1 + .../src/frontend/lowering/dynamic-tier.ts | 284 ++++++++++++++++++ .../compiler/src/frontend/lowering/lowerer.ts | 13 +- packages/compiler/src/index.ts | 18 ++ packages/compiler/src/startup-cache.ts | 1 + tests/coverage-fixtures/dynamic-tier/hot.js | 36 +++ .../dynamic-tier/package/main.ts | 13 + .../node_modules/untyped-lib/index.d.ts | 1 + .../package/node_modules/untyped-lib/index.js | 6 + .../node_modules/untyped-lib/package.json | 1 + tests/coverage-fixtures/dynamic-tier/quiet.ts | 13 + .../dynamic-tier/typed-hot.ts | 21 ++ .../__snapshots__/coverage-dynamic-tier.txt | 13 + tests/harness/coverage.test.ts | 42 +++ 29 files changed, 802 insertions(+), 21 deletions(-) create mode 100644 packages/cli/test/dynamic-tier-warning.test.ts create mode 100644 packages/compiler/src/diagnostics/dynamic-tier.ts create mode 100644 packages/compiler/src/frontend/lowering/dynamic-tier.ts create mode 100644 tests/coverage-fixtures/dynamic-tier/hot.js create mode 100644 tests/coverage-fixtures/dynamic-tier/package/main.ts create mode 100644 tests/coverage-fixtures/dynamic-tier/package/node_modules/untyped-lib/index.d.ts create mode 100644 tests/coverage-fixtures/dynamic-tier/package/node_modules/untyped-lib/index.js create mode 100644 tests/coverage-fixtures/dynamic-tier/package/node_modules/untyped-lib/package.json create mode 100644 tests/coverage-fixtures/dynamic-tier/quiet.ts create mode 100644 tests/coverage-fixtures/dynamic-tier/typed-hot.ts create mode 100644 tests/harness/__snapshots__/coverage-dynamic-tier.txt diff --git a/.gitignore b/.gitignore index fda9414aae..cd44fe11d6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ node_modules/ !tests/corpus/*/node_modules/ !tests/fixtures/npm-static/node_modules/ !tests/coverage-fixtures/wasm/node_modules/ +!tests/coverage-fixtures/dynamic-tier/package/node_modules/ !tests/fixtures/npm-static/nested/inner/node_modules/ !tests/fixtures/npm-static/node_modules/*/node_modules/ !tests/fixtures/npm/store/*/node_modules/ diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index efdb03d7d1..8c57d2e2a7 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -114,8 +114,8 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
--print <native-link-info|diagnostics>
native-link-info: build an object (equivalent to --emit=obj) and print its machine-readable external link recipe as JSON instead of printing the artifact path. The document names the exact installed precompiled runtime pack and all link inputs, but does not invoke a linker. diagnostics: for build and coverage, print the versioned JSON diagnostics document described in Coverage Reports instead of the human output.
-
--fail-on <blockers|divergences>
-
Coverage only. Exit with status 1 when the entry has blockers that would fail a build, or with divergences, also when it has Node divergence warnings. See Gating builds and CI.
+
--fail-on <blockers|divergences|dynamic>
+
Coverage only. Exit with status 1 when the entry has blockers that would fail a build, with divergences, also when it has Node divergence warnings, or with dynamic, also when it has the SC6101 warning for untyped code that likely runs often. See Gating builds and CI.
--ffi <file>
Bind signature-only TypeScript declarations to native C ABI symbols and link the manifest's archive, object, and system-library inputs. See Native FFI.
@@ -156,6 +156,9 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
--explain-dynamic[=<name>]
Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as x, Point, or Point.x, show only the matching units. See Dynamic tier.
+
--no-dynamic-tier-warning
+
Build and coverage. Leave out SC6101, the warning that names functions that likely run often and use untyped values. Not allowed with --fail-on=dynamic. See Untyped code that likely runs often.
+
--wasm <file.wasm>
Coverage only. Report a WebAssembly module the program loads from a path its sources do not spell out. The report already lists modules whose paths the compiler resolves, modules inlined as base64 literals, and .wasm files shipped by packages that use WebAssembly; a scriptc.wasm array in package.json adds files the same way as this flag. Each module is listed with its size, whether it validates, and the WebAssembly proposals it uses. Builds translate the modules they find to native code; a build reads the scriptc.wasm array but not this flag. Repeatable.
diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index 1bb645bd93..09f87bacdc 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -62,6 +62,25 @@ Code that operates on values whose types are known only at runtime, such as `any `--tier=static-only` keeps every unsupported construct a blocker instead of moving it to the dynamic tier; code that already operates on runtime-typed values compiles as before. Today `auto`, the default, behaves the same way. +### Untyped code that likely runs often + +When functions of the program's own files use untyped values and likely run often, `scriptc build` and coverage report them as an `SC6101` warning. For the Benchmarks Game n-body program in plain JavaScript: + +```console +$ scriptc build nbody.js -o nbody +warning SC6101: 3 functions that likely run often use untyped values, which runs slower than typed code: + advance (nbody.js:22): untyped JavaScript file, in a loop + energy (nbody.js:40): untyped JavaScript file, in a loop + offsetMomentum (nbody.js:12): untyped JavaScript file, in a loop + hint: add types to compile these statically; see scriptc coverage --explain-dynamic + +The program compiled. Adding types to these functions lets scriptc compile them to faster code. +``` + +Without a profile, a dynamic-tier function counts as likely to run often when a dynamic operation runs inside one of its loops, when a loop calls it, or when it calls itself. A function is named only when its source says why its values are untyped: a value typed `any` (for a `for...of` variable, the value it iterates), an untyped `JSON.parse` result, or an untyped JavaScript file. Dynamic code that runs once, such as setup code that reads an `any` configuration, is not reported, and neither are functions that are dynamic for an operation with no static lowering, which more types would not change. + +The warning names at most five functions, most likely to run often first, and gives the total. Dynamic-tier code in npm packages is summarized on one line with each package's function count. A build that comes from the cache repeats the warning. `--no-dynamic-tier-warning` leaves it out, and `--fail-on=dynamic` fails a coverage run on it. + ## Diagnostic codes Coverage and `scriptc build` report the same diagnostic codes. Common ones include: @@ -88,7 +107,7 @@ By default, `scriptc coverage` exits with status 0 whenever the program type-che $ scriptc coverage src/main.ts --fail-on=blockers ``` -`--fail-on=blockers` exits with status 1 when the program has blockers that would fail `scriptc build`. Blockers in unreached code and deferred operations don't count. `--fail-on=divergences` also fails on Node divergences. A program that doesn't type-check always exits with status 1. +`--fail-on=blockers` exits with status 1 when the program has blockers that would fail `scriptc build`. Blockers in unreached code and deferred operations don't count. `--fail-on=divergences` also fails on Node divergences, and `--fail-on=dynamic` also fails on the `SC6101` warning for [untyped code that likely runs often](#untyped-code-that-likely-runs-often). A program that doesn't type-check always exits with status 1. Coverage caches its result. Running it again on an unchanged program, or after edits that only touch comments, reuses the previous verdict. Set `SCRIPTC_NO_CACHE=1` to turn the cache off. @@ -174,7 +193,7 @@ For editors, CI annotations, and other tools, `scriptc coverage --print=diagnost category - invalid-source (the program has a TypeScript or JSON error), unsupported (valid code scriptc cannot compile yet), divergence (compiles, but can behave differently from Node.js), environment (project configuration, scriptc's type world, toolchain, or target), or internal (a compiler bug). + invalid-source (the program has a TypeScript or JSON error), unsupported (valid code scriptc cannot compile yet), divergence (compiles, but can behave differently from Node.js), performance (compiles and behaves as under Node.js, but runs slower than typed code would), environment (project configuration, scriptc's type world, toolchain, or target), or internal (a compiler bug). severity diff --git a/packages/cli/src/bootstrap.ts b/packages/cli/src/bootstrap.ts index 1a319f98da..242a7c6f7d 100644 --- a/packages/cli/src/bootstrap.ts +++ b/packages/cli/src/bootstrap.ts @@ -160,6 +160,10 @@ async function tryFastPath(): Promise { if (hit === null) return null; if (!values["keep-llvm"]) await rm(hit.llvmPath, { force: true }); if (command === "build") { + // A cached build repeats its performance warning (SC6101). + const performance = hit.performance ?? []; + if (performance.length > 0 && values["dynamic-tier-warning"] !== false) + process.stderr.write(startup.renderPerformanceWarnings(performance) + "\n"); process.stdout.write(`${outPath}\n`); return 0; } @@ -217,6 +221,7 @@ async function coverageFastPath( const hit = await startup.readCoverageVerdict(resolve(inputArg), {}).catch(() => null); if (hit === null) return null; const { coverage, sourceTexts } = hit; + if (values["dynamic-tier-warning"] === false) delete coverage.performance; if (print === "diagnostics") { const envelope = startup.coverageEnvelope(coverage, { compilerVersion: packageVersion(), diff --git a/packages/cli/test/dynamic-tier-warning.test.ts b/packages/cli/test/dynamic-tier-warning.test.ts new file mode 100644 index 0000000000..67de774d45 --- /dev/null +++ b/packages/cli/test/dynamic-tier-warning.test.ts @@ -0,0 +1,120 @@ +import { execFile } from "node:child_process"; +import { createRequire } from "node:module"; +import { mkdir, mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; + +const execFileAsync = promisify(execFile); +const require = createRequire(import.meta.url); +const repoRoot = join(import.meta.dirname, "../../.."); +const cliEntry = join(repoRoot, "packages/cli/src/main.ts"); +const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs"); +const fixtures = join(repoRoot, "tests/coverage-fixtures/dynamic-tier"); + +const HEADER = + "warning SC6101: 3 functions that likely run often use untyped values, which runs slower than typed code:"; +const CLOSING = + "The program compiled. Adding types to these functions lets scriptc compile them to faster code."; + +async function cli( + args: string[], + cacheRoot: string, +): Promise<{ code: number; stdout: string; stderr: string }> { + try { + const { stdout, stderr } = await execFileAsync( + process.execPath, + ["--import", tsxLoader, cliEntry, ...args], + { + env: { ...process.env, SCRIPTC_CACHE_DIR: cacheRoot, SCRIPTC_TIMING: "1" }, + maxBuffer: 4 * 1024 * 1024, + }, + ); + return { code: 0, stdout, stderr }; + } catch (err) { + const e = err as { code?: number; stdout?: string; stderr?: string }; + return { code: e.code ?? 1, stdout: e.stdout ?? "", stderr: e.stderr ?? "" }; + } +} + +test("a build names its likely-hot untyped functions, also when it comes from the cache", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-dynamic-tier-warning-")); + const cacheRoot = join(dir, "cache"); + const outPath = join(dir, process.platform === "win32" ? "program.exe" : "program"); + try { + await mkdir(cacheRoot, { mode: 0o700 }); + const build = (extra: string[] = []) => + cli(["build", join(fixtures, "hot.js"), "-o", outPath, ...extra], cacheRoot); + + const first = await build(); + expect(first.code).toBe(0); + expect(first.stderr).toContain("scriptc lowering"); + expect(first.stderr).toContain( + [ + HEADER, + " centroid (hot.js:7): untyped JavaScript file, in a loop", + " spread (hot.js:23): untyped JavaScript file, in a loop", + " distance (hot.js:17): untyped JavaScript file, called in a loop", + " hint: add types to compile these statically; see scriptc coverage --explain-dynamic", + "", + CLOSING, + ].join("\n"), + ); + + // The cache hit skips lowering and repeats the warning. + const cached = await build(); + expect(cached.code).toBe(0); + expect(cached.stderr).not.toContain("scriptc lowering"); + expect(cached.stderr).toContain(HEADER); + expect(cached.stderr).toContain(CLOSING); + + const silenced = await build(["--no-dynamic-tier-warning"]); + expect(silenced.code).toBe(0); + expect(silenced.stderr).not.toContain("SC6101"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 300_000); + +test("untyped setup code builds without the warning", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-dynamic-tier-quiet-")); + try { + await mkdir(join(dir, "cache"), { mode: 0o700 }); + const result = await cli( + ["build", join(fixtures, "quiet.ts"), "-o", join(dir, "program")], + join(dir, "cache"), + ); + expect(result.code).toBe(0); + expect(result.stderr).not.toContain("SC6101"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 300_000); + +test("coverage --fail-on=dynamic fails on the warning, and --no-dynamic-tier-warning cannot hide it", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-dynamic-tier-fail-on-")); + try { + await mkdir(join(dir, "cache"), { mode: 0o700 }); + const coverage = (file: string, extra: string[]) => + cli(["coverage", join(fixtures, file), ...extra], join(dir, "cache")); + + const hot = await coverage("hot.js", ["--fail-on=dynamic"]); + expect(hot.code).toBe(1); + expect(hot.stdout).toContain("untyped and likely hot"); + expect((await coverage("hot.js", ["--fail-on=divergences"])).code).toBe(0); + expect((await coverage("quiet.ts", ["--fail-on=dynamic"])).code).toBe(0); + + const silenced = await coverage("hot.js", ["--no-dynamic-tier-warning"]); + expect(silenced.code).toBe(0); + expect(silenced.stdout).not.toContain("untyped and likely hot"); + + const conflict = await coverage("hot.js", ["--fail-on=dynamic", "--no-dynamic-tier-warning"]); + expect(conflict.code).toBe(1); + expect(conflict.stderr).toContain( + "--fail-on=dynamic checks the warning --no-dynamic-tier-warning leaves out", + ); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 300_000); diff --git a/packages/compiler/src/cli/command.ts b/packages/compiler/src/cli/command.ts index 03160c6fac..3285aa7fa6 100644 --- a/packages/compiler/src/cli/command.ts +++ b/packages/compiler/src/cli/command.ts @@ -13,6 +13,7 @@ import { import { TIER_MODES, type TierMode } from "../frontend/tier-analysis.js"; import { buildEnvelope } from "../diagnostics/envelope.js"; import { renderWarnings } from "../diagnostics/render.js"; +import { isPerformanceCode, renderPerformanceWarnings } from "../diagnostics/dynamic-tier.js"; import { isNativeOptimization } from "../backend/optimization.js"; import { setProvenanceSources } from "../frontend/provenance-registry.js"; import type { CliHost, NativeCacheWarmProfile } from "./host.js"; @@ -284,8 +285,20 @@ async function main(args: string[], host: CliHost): Promise { fail(`--print=diagnostics is a build/coverage option\n\n${USAGE}`); } const failOnRaw = values["fail-on"]; - if (failOnRaw !== undefined && failOnRaw !== "blockers" && failOnRaw !== "divergences") { - fail(`unknown --fail-on value "${failOnRaw}" (supported: blockers, divergences)\n\n${USAGE}`); + if ( + failOnRaw !== undefined && + failOnRaw !== "blockers" && + failOnRaw !== "divergences" && + failOnRaw !== "dynamic" + ) { + fail( + `unknown --fail-on value "${failOnRaw}" (supported: blockers, divergences, dynamic)\n\n${USAGE}`, + ); + } + // --no-dynamic-tier-warning leaves out SC6101 (diagnostics/dynamic-tier.ts). + const dynamicTierWarning = values["dynamic-tier-warning"] !== false; + if (failOnRaw === "dynamic" && !dynamicTierWarning) { + fail(`--fail-on=dynamic checks the warning --no-dynamic-tier-warning leaves out\n\n${USAGE}`); } if (failOnRaw !== undefined && command !== "coverage") { fail(`--fail-on is a coverage option (a build already fails on blockers)\n\n${USAGE}`); @@ -430,6 +443,7 @@ async function main(args: string[], host: CliHost): Promise { ...(explainDynamic !== undefined ? { explainDynamic: true } : {}), ...(wasmFiles.length > 0 ? { wasm: wasmFiles } : {}), }); + if (!dynamicTierWarning) delete coverage.performance; if (printDiagnostics) { // Keep stdout pure JSON for tooling. const envelope = coverageEnvelope(coverage, { @@ -517,7 +531,9 @@ async function main(args: string[], host: CliHost): Promise { process.stderr.write(`\n${n} error${n === 1 ? "" : "s"}.\n`); throw new CliExit(1); } - const warnings = result.warnings ?? []; + const warnings = (result.warnings ?? []).filter( + (d) => dynamicTierWarning || !isPerformanceCode(d.code), + ); if (printDiagnostics) { diagnosticsEnvelope = buildEnvelope({ compilerVersion: host.version(), @@ -530,12 +546,20 @@ async function main(args: string[], host: CliHost): Promise { }); } else if (warnings.length > 0 && command === "build") { // Divergence warnings never fail a build; they explain where the - // native program can behave differently from Node. `run` keeps - // stderr for the program's own output. + // native program can behave differently from Node. The performance + // warning names likely-hot functions that use untyped values. `run` + // keeps stderr for the program's own output. const color = process.stderr.isTTY ?? false; - process.stderr.write( - renderWarnings(warnings, result.sourceTexts ?? new Map(), { color }) + "\n", - ); + const divergences = warnings.filter((d) => !isPerformanceCode(d.code)); + const performance = warnings.filter((d) => isPerformanceCode(d.code)); + if (divergences.length > 0) + process.stderr.write( + renderWarnings(divergences, result.sourceTexts ?? new Map(), { color }) + "\n", + ); + if (performance.length > 0) { + if (divergences.length > 0) process.stderr.write("\n"); + process.stderr.write(renderPerformanceWarnings(performance) + "\n"); + } } if (result.artifact.kind === "exe") { if (!values["keep-llvm"]) rmSync(result.artifact.translationUnitPath, { force: true }); diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts index aa7408d2a2..23e57c66ff 100644 --- a/packages/compiler/src/cli/usage.ts +++ b/packages/compiler/src/cli/usage.ts @@ -31,10 +31,11 @@ Options: (native-link-info implies --emit=obj and never links; diagnostics prints the versioned JSON diagnostics envelope for build and coverage) - --fail-on + --fail-on coverage only: exit 1 when the entry path has blockers (a build would fail), or also when it has Node - divergences + divergences, or also when it has likely-hot functions + that use untyped values (SC6101) --backend code generator (llvm) --optimization native optimization posture (default: release/-O2). dev @@ -100,6 +101,9 @@ Options: values and the bindings, fields, functions, classes and modules the dynamic tier would take over, with the refusal behind each; a name shows only that unit's chain + --no-dynamic-tier-warning + build and coverage: leave out SC6101, the warning that + names likely-hot functions that use untyped values --wasm coverage only: report a WebAssembly module the program loads whose path its sources do not spell (repeatable; @@ -131,6 +135,7 @@ export const CLI_OPTIONS = { "external-types": { type: "string", multiple: true }, tier: { type: "string" }, "explain-dynamic": { type: "string" }, + "dynamic-tier-warning": { type: "boolean", default: true }, wasm: { type: "string", multiple: true }, lib: { type: "boolean", default: false }, profile: { type: "string" }, diff --git a/packages/compiler/src/coverage/report.ts b/packages/compiler/src/coverage/report.ts index 89733e255e..37f77f4c69 100644 --- a/packages/compiler/src/coverage/report.ts +++ b/packages/compiler/src/coverage/report.ts @@ -42,6 +42,10 @@ export interface CoverageInput { * the program compiles, but these sites can behave differently natively. * Never blockers; `--fail-on=divergences` opts into failing on them. */ divergences?: ScrDiagnostic[]; + /** The performance warning (SC6101): likely-hot functions the source + * leaves untyped. Never blockers; `--fail-on=dynamic` opts into failing + * on it. */ + performance?: ScrDiagnostic[]; /** Each dependency's native compilation outcome, with the first refusal * reason for a refused package. */ npmStatic?: NpmStaticStatus[]; @@ -333,6 +337,17 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions ); renderRows(groupBlockers(divergences), YELLOW); }; + // The performance warning: its message names the functions. + const performance = input.performance ?? []; + const renderPerformance = () => { + for (const d of performance) { + out.push(""); + const [head, ...rest] = d.message.split("\n"); + out.push(` ${c(YELLOW, "untyped and likely hot")} ${head}`); + for (const line of rest) out.push(` ${line}`); + if (d.hint !== undefined) out.push(` ${c(DIM, `hint: ${d.hint}`)}`); + } + }; const unreachedDiags = [...(un?.diagnostics ?? []), ...(un?.runtimeFences ?? [])]; if (failed === 0 && input.diagnostics.length === 0 && unreachedDiags.length === 0) { @@ -341,6 +356,7 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions out.push(""); renderDivergences(); } + renderPerformance(); return out.join("\n"); } @@ -381,6 +397,7 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions out.push(""); renderDivergences(); } + renderPerformance(); return out.join("\n"); } @@ -449,8 +466,9 @@ function displayPath(file: string, root: string | undefined): string { } /** What `coverage --fail-on` checks: reached blockers (they fail a build) - * and, optionally, divergences. Unreached blockers never fail. */ -export type CoverageFailOn = "blockers" | "divergences"; + * and, optionally, divergences or the performance warning. Unreached + * blockers never fail. */ +export type CoverageFailOn = "blockers" | "divergences" | "dynamic"; /** Whether the analysis passes the selected gate. Preflight failures * always fail; `failOn` undefined means only preflight fails (the report's @@ -462,6 +480,7 @@ export function coveragePasses(input: CoverageInput, failOn?: CoverageFailOn): b // runtime fences build (the statement throws only if it executes). if (input.diagnostics.length > 0) return false; if (failOn === "divergences" && (input.divergences?.length ?? 0) > 0) return false; + if (failOn === "dynamic" && (input.performance?.length ?? 0) > 0) return false; return true; } @@ -492,6 +511,7 @@ export function coverageEnvelope( add(input.unreached?.diagnostics, "unreached"); add(input.unreached?.runtimeFences, "deferred"); add(input.divergences, "reached"); + add(input.performance, "reached"); } const un = input.unreached; const total = input.stats.statementsTotal + (un?.stats.statementsTotal ?? 0); diff --git a/packages/compiler/src/coverage/verdict-cache.ts b/packages/compiler/src/coverage/verdict-cache.ts index 422bf5e4df..e5fa7242e9 100644 --- a/packages/compiler/src/coverage/verdict-cache.ts +++ b/packages/compiler/src/coverage/verdict-cache.ts @@ -38,6 +38,7 @@ export interface CachedCoverage { runtimeFences?: ScrDiagnostic[]; unreached?: { stats: LowerStats; diagnostics: ScrDiagnostic[]; runtimeFences?: ScrDiagnostic[] }; divergences?: ScrDiagnostic[]; + performance?: ScrDiagnostic[]; npmStatic?: NpmStaticStatus[]; wasmModules?: WasmModuleReport[]; preflightFailed: boolean; @@ -149,6 +150,7 @@ function cacheable(coverage: AnalyzeResult["coverage"]): CachedCoverage | null { if (coverage.runtimeFences !== undefined) out.runtimeFences = coverage.runtimeFences; if (coverage.unreached !== undefined) out.unreached = coverage.unreached; if (coverage.divergences !== undefined) out.divergences = coverage.divergences; + if (coverage.performance !== undefined) out.performance = coverage.performance; if (coverage.npmStatic !== undefined) out.npmStatic = coverage.npmStatic; if (coverage.wasmModules !== undefined) out.wasmModules = coverage.wasmModules; return out; @@ -166,6 +168,7 @@ function restore(cached: CachedCoverage, sources: CachedSource[]): AnalyzeResult if (cached.runtimeFences !== undefined) coverage.runtimeFences = cached.runtimeFences; if (cached.unreached !== undefined) coverage.unreached = cached.unreached; if (cached.divergences !== undefined) coverage.divergences = cached.divergences; + if (cached.performance !== undefined) coverage.performance = cached.performance; if (cached.npmStatic !== undefined) coverage.npmStatic = cached.npmStatic; if (cached.wasmModules !== undefined) coverage.wasmModules = cached.wasmModules; return { coverage, sourceTexts }; diff --git a/packages/compiler/src/coverage/verdict-semantic.ts b/packages/compiler/src/coverage/verdict-semantic.ts index cc1c1f4148..ad2f4d180c 100644 --- a/packages/compiler/src/coverage/verdict-semantic.ts +++ b/packages/compiler/src/coverage/verdict-semantic.ts @@ -7,6 +7,9 @@ import { rebaseSourceLocations, semanticallyEqualSource } from "../library/seman import type { SemanticReplay } from "./verdict-cache.js"; export const commentOnlyReplay: SemanticReplay = (probes, sources, coverage) => { + // The performance warning's text names file:line positions, which moving + // its locations would not update: such a verdict is analyzed again. + if (coverage.performance !== undefined) return null; const previous = new Map(); for (const source of sources) previous.set(source.path, source.text); const match = frontendInputsSemanticallyMatch(probes, previous, semanticallyEqualSource); diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index e4d4006cad..17cb2efc09 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -61,7 +61,10 @@ * collation does not cover (SC6003), a call feeding an instance * to a cast to an unrelated class (SC6004); SC6005 (Date strings * outside the native parser's grammar) is retired — the runtime - * parses date strings with V8's grammar + * parses date strings with V8's grammar. SC61xx are performance + * notes, also warnings (coverage --fail-on=dynamic opts in): + * likely-hot functions that use untyped values (SC6101, + * diagnostics/dynamic-tier.ts) * SC9xxx internal compiler errors (still source-anchored) */ import type { SrcLoc } from "../ir/ir.js"; diff --git a/packages/compiler/src/diagnostics/dynamic-tier.ts b/packages/compiler/src/diagnostics/dynamic-tier.ts new file mode 100644 index 0000000000..7970d3f42a --- /dev/null +++ b/packages/compiler/src/diagnostics/dynamic-tier.ts @@ -0,0 +1,99 @@ +/* SC6101, the dynamic-tier performance warning: its text and how it prints. + * All of the warning's wording is here. The functions it names come from + * frontend/lowering/dynamic-tier.ts; this module only formats them, and it + * imports nothing heavy so the CLI's routed cache hit can print a stored + * warning without loading the compiler. */ +import type { SrcLoc } from "../ir/ir.js"; +import type { ScrDiagnostic } from "./diagnostic.js"; + +/** The code of the warning; SC61xx are performance notes, SC60xx divergences. */ +export const DYNAMIC_TIER_CODE = "SC6101"; + +/** At most this many functions are named; the count covers all of them. */ +export const DYNAMIC_TIER_SHOWN = 5; + +/** Why a function counts as likely to run often. */ +export type HotReason = "loop" | "called-in-loop" | "recursive"; + +const HOT_PHRASE: Record = { + loop: "in a loop", + "called-in-loop": "called in a loop", + recursive: "recursive", +}; + +/** Why a function's values are untyped, as the warning words it. */ +export type UntypedCause = + | { kind: "any"; name: string } + | { kind: "json-binding"; name: string } + | { kind: "json" } + | { kind: "javascript" }; + +export function causePhrase(cause: UntypedCause): string { + switch (cause.kind) { + case "any": + return `\`${cause.name}\` is \`any\``; + case "json-binding": + return `\`${cause.name}\` holds an untyped \`JSON.parse\` result`; + case "json": + return "untyped `JSON.parse` result"; + case "javascript": + return "untyped JavaScript file"; + } +} + +export interface DynamicTierEntry { + /** Display name (`advance`, `Parser.next`). */ + name: string; + /** `file:line`, relative to the entry's directory. */ + where: string; + cause: UntypedCause; + hot: HotReason; +} + +export interface DynamicTierPackageCount { + name: string; + functions: number; +} + +/** The SC6101 diagnostic for likely-hot functions with untyped values, or + * null when there are none. `loc` is the first function's declaration. */ +export function dynamicTierDiag( + entries: readonly DynamicTierEntry[], + packages: readonly DynamicTierPackageCount[], + loc: SrcLoc, +): ScrDiagnostic | null { + const n = entries.length; + if (n === 0) return null; + const shown = entries.slice(0, DYNAMIC_TIER_SHOWN); + const lines = [ + `${n} function${n === 1 ? "" : "s"} that likely run${n === 1 ? "s" : ""} often use${n === 1 ? "s" : ""} untyped values, which runs slower than typed code:`, + ...shown.map((e) => ` ${e.name} (${e.where}): ${causePhrase(e.cause)}, ${HOT_PHRASE[e.hot]}`), + ...(n > shown.length ? [` and ${n - shown.length} more`] : []), + ...(packages.length === 0 + ? [] + : [ + ` untyped code in npm packages: ${packages + .map((p) => `${p.name} (${p.functions} function${p.functions === 1 ? "" : "s"})`) + .join(", ")}`, + ]), + ]; + return { + code: DYNAMIC_TIER_CODE, + message: lines.join("\n"), + loc, + hint: "add types to compile these statically; see scriptc coverage --explain-dynamic", + }; +} + +export function isPerformanceCode(code: string): boolean { + return /^SC61\d{2}$/.test(code); +} + +/** The warning as a build prints it: no source excerpt (it names several + * functions), then its closing line. */ +export function renderPerformanceWarnings(diags: readonly ScrDiagnostic[]): string { + const blocks = diags.map( + (d) => `warning ${d.code}: ${d.message}${d.hint === undefined ? "" : `\n hint: ${d.hint}`}`, + ); + return `${blocks.join("\n\n")}\n\nThe program compiled. Adding types to these functions lets scriptc compile them to faster code.`; +} diff --git a/packages/compiler/src/diagnostics/envelope.ts b/packages/compiler/src/diagnostics/envelope.ts index c986fc5b17..59c285eadb 100644 --- a/packages/compiler/src/diagnostics/envelope.ts +++ b/packages/compiler/src/diagnostics/envelope.ts @@ -16,6 +16,7 @@ * bumps it. */ import { readFileSync } from "node:fs"; import type { ScrDiagnostic } from "./diagnostic.js"; +import { isPerformanceCode } from "./dynamic-tier.js"; import type { WasmModuleReport } from "../wasm/discover.js"; export const DIAGNOSTICS_SCHEMA = "scriptc-diagnostics"; @@ -25,12 +26,14 @@ export const DIAGNOSTICS_SCHEMA_VERSION = 1; * - invalid-source: the program itself is wrong (TypeScript errors, bad JSON modules) * - unsupported: valid TypeScript/JavaScript scriptc cannot compile yet * - divergence: compiles, but the native program behaves differently from Node + * - performance: compiles and behaves as under Node, but runs slower than typed code would * - environment: project configuration, scriptc's type world, toolchain or target * - internal: a compiler bug */ export type DiagnosticCategory = | "invalid-source" | "unsupported" | "divergence" + | "performance" | "environment" | "internal"; @@ -149,12 +152,13 @@ export interface DiagnosticsEnvelope { /** The divergence band: compiles, but the program can behave differently * from Node. Warnings, never build failures. */ export function isDivergenceCode(code: string): boolean { - return /^SC6\d{3}$/.test(code); + return /^SC60\d{2}$/.test(code); } export function categoryOf(diag: ScrDiagnostic): DiagnosticCategory { const code = diag.code; if (isDivergenceCode(code)) return "divergence"; + if (isPerformanceCode(code)) return "performance"; if (code === "SC0001") return diag.typeWorld === true ? "environment" : "invalid-source"; if (code === "SC0003") return "invalid-source"; if (code === "SC0002" || code === "SC0004") return "environment"; @@ -274,7 +278,7 @@ export function locateDiagnostic( return { code: diag.code, category: categoryOf(diag), - severity: isDivergenceCode(diag.code) ? "warning" : "error", + severity: isDivergenceCode(diag.code) || isPerformanceCode(diag.code) ? "warning" : "error", scope, file: diag.loc.file, line: start.line, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index c1e3d87255..2312f5e85a 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -11,6 +11,7 @@ import { writeFile, } from "node:fs/promises"; import { basename, dirname, join, resolve } from "node:path"; +import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; import type { FrontendInputSnapshot } from "../frontend/input-tracker.js"; import { frontendInputsStillMatch, validFrontendInputSnapshot } from "../frontend/input-tracker.js"; import { compilerReleaseVersion } from "../library/sidecar.js"; @@ -112,6 +113,8 @@ interface EarlyExecutableCacheStamp { }; nativeDependencies: NativeArtifactDependency[] | null; native: EarlyExecutableNativeFeatures; + /** The build's performance warnings (SC61xx), which a cache hit repeats. */ + performance?: ScrDiagnostic[]; integrity: string; } @@ -181,6 +184,8 @@ export interface EarlyExecutableCacheHit { executableRestored: boolean; native: EarlyExecutableNativeFeatures; frontend: FrontendInputSnapshot; + /** The performance warnings the build reported (SC61xx), repeated on a hit. */ + performance?: ScrDiagnostic[]; } export interface EarlyExecutableCachePublish extends EarlyExecutableCacheHit { @@ -610,6 +615,9 @@ async function readExecutableCache( native: stamp.native, executableRestored, frontend: stamp.frontend, + ...(Array.isArray(stamp.performance) && stamp.performance.length > 0 + ? { performance: stamp.performance } + : {}), ...(ir === null ? {} : { irPath: paths.irPath }), }; } catch { @@ -793,6 +801,9 @@ export async function publishEarlyExecutableCache( }, nativeDependencies: executable === null ? null : result.nativeDependencies!, native: result.native, + ...(result.performance === undefined || result.performance.length === 0 + ? {} + : { performance: result.performance }), }; const stamp: EarlyExecutableCacheStamp = { ...unsigned, integrity: stampIntegrity(unsigned) }; await writeFile(join(stage, "stamp.json"), `${JSON.stringify(stamp)}\n`, { mode: 0o600 }); diff --git a/packages/compiler/src/executable/prepare.ts b/packages/compiler/src/executable/prepare.ts index 4afae7c7d0..1771f98f58 100644 --- a/packages/compiler/src/executable/prepare.ts +++ b/packages/compiler/src/executable/prepare.ts @@ -122,6 +122,6 @@ export function prepareExecutableModule( mod: lowered.module!, sourceTexts, stats: lowered.stats, - warnings: lowered.divergences ?? [], + warnings: [...(lowered.divergences ?? []), ...(lowered.performance ?? [])], }; } diff --git a/packages/compiler/src/frontend/analysis.ts b/packages/compiler/src/frontend/analysis.ts index 43d0aa2bb5..776492eb1a 100644 --- a/packages/compiler/src/frontend/analysis.ts +++ b/packages/compiler/src/frontend/analysis.ts @@ -106,6 +106,7 @@ export function analyzeWithFrontend( diagnostics: [...preflight, ...lowered.diagnostics], ...(lowered.runtimeFences.length > 0 ? { runtimeFences: lowered.runtimeFences } : {}), ...(lowered.divergences !== undefined ? { divergences: lowered.divergences } : {}), + ...(lowered.performance !== undefined ? { performance: lowered.performance } : {}), ...(lowered.unreached ? { unreached: lowered.unreached } : {}), ...(fe.npmStatic.length > 0 ? { npmStatic: fe.npmStatic } : {}), // --provenance-sources: the per-package attribution inputs (the diff --git a/packages/compiler/src/frontend/lowering/dynamic-tier.ts b/packages/compiler/src/frontend/lowering/dynamic-tier.ts new file mode 100644 index 0000000000..873b223432 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/dynamic-tier.ts @@ -0,0 +1,284 @@ +/* Hot dynamic-tier functions (SC6101, diagnostics/dynamic-tier.ts): the + * functions of a program that compile to the dynamic tier (ir/tiers.ts) and + * likely run often, so a build can name them. Without a profile, a function + * counts as likely hot when a dynamic operation runs inside one of its + * loops, when a loop calls it, or when it calls itself. Dynamic code that + * runs once (setup code that reads an `any` configuration, for example) + * stays quiet. + * + * The tier assignment is the one lowering made (Lowerer.dynamicFunctions); + * this module ranks its functions and says why each one is dynamic, from + * the source at its dynamic operations. A function is named only when the + * source says why (an `any`, an untyped `JSON.parse` result, an untyped + * JavaScript file): functions dynamic for operations without a static + * lowering would stay dynamic with more types. */ +import { relative } from "node:path"; +import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; +import { + dynamicTierDiag, + type DynamicTierPackageCount, + type HotReason, + type UntypedCause, +} from "../../diagnostics/dynamic-tier.js"; +import type { IrExpr, IrFunction, IrModule, IrStmt, SrcLoc } from "../../ir/ir.js"; +import { dynamicOperationSites } from "../../ir/tiers.js"; +import { everyStmtList } from "../../ir/traverse.js"; +import * as ts from "../ts7/adapter.js"; +import { isCheckedJsSourceFile, isJsSourceFile } from "../program.js"; +import { isNodeModulesPath } from "../resolve.js"; +import type { Lowerer } from "./lowerer.js"; + +export interface HotDynamicFunction { + /** The IR function name (`%Point.norm`, `parse`, ...). */ + name: string; + loc: SrcLoc; + hot: HotReason; + /** Dynamic operations inside the function's loops. */ + loopOperations: number; + cause: UntypedCause; +} + +export interface DynamicTierReport { + /** Likely-hot dynamic functions in the program's own files, most likely + * hot first. */ + hot: HotDynamicFunction[]; + /** Dynamic-tier functions in npm packages, by package. */ + packages: DynamicTierPackageCount[]; +} + +const LOOP_KINDS = new Set(["while", "doWhile", "for", "forOf"]); + +function within(loc: SrcLoc, spans: readonly SrcLoc[]): boolean { + return spans.some((s) => s.file === loc.file && s.start <= loc.start && loc.end <= s.end); +} + +interface FunctionFacts { + loops: SrcLoc[]; + /** Callee names of direct calls, with each call's location. */ + calls: { callee: string; loc: SrcLoc }[]; +} + +function factsOf(fn: IrFunction): FunctionFacts { + const facts: FunctionFacts = { loops: [], calls: [] }; + everyStmtList(fn.body, { + stmt: (s: IrStmt) => { + if (LOOP_KINDS.has(s.kind)) facts.loops.push(s.loc); + return true; + }, + expr: (e: IrExpr) => { + if (e.kind === "call") facts.calls.push({ callee: e.callee, loc: e.loc }); + return true; + }, + }); + return facts; +} + +/** The package directory name of a node_modules path (`@scope/name` or `name`). */ +function packageOf(file: string): string { + const parts = file.split("/"); + const at = parts.lastIndexOf("node_modules"); + const first = parts[at + 1] ?? ""; + return first.startsWith("@") ? `${first}/${parts[at + 2] ?? ""}` : first; +} + +/** The innermost node spanning [start, end). */ +function nodeAt(sf: ts.SourceFile, start: number, end: number): ts.Node { + let node: ts.Node = sf; + for (;;) { + let next: ts.Node | undefined; + ts.forEachChild(node, (child) => { + if (ts.tokenStartOfNode(child) <= start && end <= child.end) { + next = child; + return true; + } + return undefined; + }); + if (next === undefined) return node; + node = next; + } +} + +/** The identifier an operand reads through (`items` in `items[i].x`). */ +function rootIdentifier(node: ts.Node): ts.Identifier | null { + let n: ts.Node = node; + for (;;) { + if (ts.isIdentifier(n)) return n; + if ( + ts.isPropertyAccessExpression(n) || + ts.isElementAccessExpression(n) || + ts.isCallExpression(n) + ) + n = n.expression; + else if (ts.isParenthesizedExpression(n) || ts.isNonNullExpression(n)) n = n.expression; + else return null; + } +} + +function isJsonParse(node: ts.Node | undefined): boolean { + if (node === undefined || !ts.isCallExpression(node)) return false; + const callee = node.expression; + return ( + ts.isPropertyAccessExpression(callee) && + ts.isIdentifier(callee.expression) && + callee.expression.text === "JSON" && + callee.name.text === "parse" + ); +} + +/** Why an identifier holds a value without a static type, or null. */ +function identifierCause(lowerer: Lowerer, id: ts.Identifier, depth = 0): UntypedCause | null { + const symbol = lowerer.checker.getSymbolAtLocation(id); + const decl = symbol?.declarations?.[0]; + // An unannotated `for (const item of items)` variable: name `items`. + if ( + depth < 4 && + decl !== undefined && + ts.isVariableDeclaration(decl) && + decl.type === undefined && + ts.isVariableDeclarationList(decl.parent) && + ts.isForOfStatement(decl.parent.parent) + ) { + const iterated = rootIdentifier(decl.parent.parent.expression); + if (iterated !== null) { + const cause = identifierCause(lowerer, iterated, depth + 1); + if (cause !== null) return cause; + } + } + if (decl !== undefined && ts.isVariableDeclaration(decl) && isJsonParse(decl.initializer)) + return { kind: "json-binding", name: id.text }; + const type = lowerer.checker.getTypeAtLocation(id); + if ((type.flags & ts.TypeFlags.Any) !== 0) return { kind: "any", name: id.text }; + return null; +} + +/** Why a node's value has no static type, or null. */ +function nodeCause(lowerer: Lowerer, n: ts.Node): UntypedCause | null { + if (isJsonParse(n)) return { kind: "json" }; + const id = rootIdentifier(n); + return id === null ? null : identifierCause(lowerer, id); +} + +/** Why the dynamic operation at `loc` has no static type, or null when + * the source does not say. The site's own subexpressions are asked first + * (the operand of a dynamic `for...of`), then its ancestors. */ +function causeAt(lowerer: Lowerer, loc: SrcLoc): UntypedCause | null { + const sf = lowerer.program.getSourceFile(loc.file); + if (sf === undefined) return null; + if (isJsSourceFile(sf) && !isCheckedJsSourceFile(sf, lowerer.projectCheckJs)) + return { kind: "javascript" }; + const node = nodeAt(sf, loc.start, loc.end); + let found: UntypedCause | null = null; + const descend = (n: ts.Node, depth: number): void => { + if (found !== null || depth > 3) return; + found = nodeCause(lowerer, n); + if (found === null) + ts.forEachChild(n, (child) => (descend(child, depth + 1), found !== null || undefined)); + }; + descend(node, 0); + for ( + let n: ts.Node | undefined = node.parent; + found === null && n !== undefined && n !== sf; + n = n.parent + ) + found = nodeCause(lowerer, n); + return found; +} + +/** The first cause the function's dynamic operations name, loop sites + * first, or null when none names one. */ +function causeOf(lowerer: Lowerer, sites: readonly { loc: SrcLoc }[]): UntypedCause | null { + for (const site of sites) { + const cause = causeAt(lowerer, site.loc); + if (cause !== null) return cause; + } + return null; +} + +/** The likely-hot dynamic-tier functions of `module` (see the header). */ +export function scanDynamicTier(lowerer: Lowerer, module: IrModule): DynamicTierReport { + const facts = new Map(); + for (const fn of module.functions) facts.set(fn.name, factsOf(fn)); + // Functions some loop calls directly. + const calledInLoop = new Set(); + for (const fn of module.functions) { + const f = facts.get(fn.name)!; + for (const call of f.calls) if (within(call.loc, f.loops)) calledInLoop.add(call.callee); + } + const hot: HotDynamicFunction[] = []; + const packages = new Map(); + for (const fn of module.functions) { + // Generated `%dyn.*` helpers are dynamic operations lowered out of line, + // and `%init.*` is a module's top level: neither is a function to name. + if (fn.tier !== "dynamic" || fn.name.startsWith("%dyn.") || fn.name.startsWith("%init.")) + continue; + if (isNodeModulesPath(fn.loc.file)) { + const name = packageOf(fn.loc.file); + packages.set(name, (packages.get(name) ?? 0) + 1); + continue; + } + const f = facts.get(fn.name)!; + const sites = dynamicOperationSites(fn); + const inLoops = sites.filter((s) => within(s.loc, f.loops)); + const recursive = f.calls.some((c) => c.callee === fn.name); + const reason: HotReason | null = + inLoops.length > 0 + ? "loop" + : calledInLoop.has(fn.name) + ? "called-in-loop" + : recursive + ? "recursive" + : null; + if (reason === null) continue; + // Only functions the source makes dynamic are named: ones dynamic for + // operations without a static lowering would stay dynamic with more types. + const cause = causeOf(lowerer, [...inLoops, ...sites.filter((s) => !inLoops.includes(s))]); + if (cause === null) continue; + hot.push({ + name: fn.name, + loc: fn.loc, + hot: reason, + loopOperations: inLoops.length, + cause, + }); + } + const rank = { loop: 0, "called-in-loop": 1, recursive: 2 } as const; + hot.sort((a, b) => rank[a.hot] - rank[b.hot] || b.loopOperations - a.loopOperations); + return { + hot, + packages: [...packages] + .map(([name, functions]) => ({ name, functions })) + .sort((a, b) => b.functions - a.functions || (a.name < b.name ? -1 : 1)), + }; +} + +/** `%%m144.Session.finish` → `Session.finish`, `m163.find` → `find`. */ +function displayName(name: string): string { + return name.replace(/^%+/, "").replace(/^m\d+\./, ""); +} + +/** SC6101 for the program lowering produced, or null (see the header). + * Paths are relative to `root`, the entry's directory. */ +export function dynamicTierWarning( + lowerer: Lowerer, + module: IrModule, + root: string, +): ScrDiagnostic | null { + const report = scanDynamicTier(lowerer, module); + const first = report.hot[0]; + if (first === undefined) return null; + const where = (loc: SrcLoc): string => { + const sf = lowerer.program.getSourceFile(loc.file); + const line = sf === undefined ? 0 : ts.getLineAndCharacterOfPosition(sf, loc.start).line + 1; + return `${relative(root, loc.file) || loc.file}:${line}`; + }; + return dynamicTierDiag( + report.hot.map((f) => ({ + name: displayName(f.name), + where: where(f.loc), + cause: f.cause, + hot: f.hot, + })), + report.packages, + first.loc, + ); +} diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index c4d24cef17..2bd5272858 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -148,6 +148,7 @@ import { ClassProtocols } from "./class-protocols.js"; import { earlyReadVarSymbols } from "./var-hoisting.js"; import { recordTextCodecClass } from "../../ir/ir.js"; import { assignLegacyTiers, type DynamicFunction } from "../../ir/tiers.js"; +import { dynamicTierWarning } from "./dynamic-tier.js"; import { promoteJsvals } from "../../ir/jsval.js"; import { analyzeTiers, @@ -176,7 +177,7 @@ export { newFnCtx, type FnCtx } from "./function-context.js"; * - Lexical scoping is resolved here: locals get function-unique ids * ("x.0", "x.1" for shadowing); the IR is scope-flat. */ -import { resolve } from "node:path"; +import { dirname, resolve } from "node:path"; import { bindingSource } from "../binding-source.js"; import { tsgoPath } from "../dts-paths.js"; import * as ts from "../ts7/adapter.js"; @@ -786,6 +787,9 @@ export interface LowerResult { /** Compile-time-detectable behavior differences from Node (SC6xxx * divergence warnings) in the program's own modules. Never failures. */ divergences?: ScrDiagnostic[]; + /** Performance warnings (SC61xx): the likely-hot functions the source + * leaves untyped (lowering/dynamic-tier.ts). Never failures. */ + performance?: ScrDiagnostic[]; /** Functions lowering placed in the dynamic tier (ir/tiers.ts), present * when there are any. */ dynamicFunctions?: DynamicFunction[]; @@ -1162,6 +1166,13 @@ export function lowerToIr( const divergences = scanDivergences(resultLowerer, moduleOrder); timing("divergence-scan", { found: divergences.length }); if (divergences.length > 0) result = { ...result, divergences }; + // The performance warning (SC6101): likely-hot functions of the program + // that compile to the dynamic tier because the source leaves them untyped. + if (result.module !== null && resultLowerer.dynamicFunctions.length > 0) { + const hotDynamic = dynamicTierWarning(resultLowerer, result.module, dirname(entry.fileName)); + timing("dynamic-tier-scan", { found: hotDynamic === null ? 0 : 1 }); + if (hotDynamic !== null) result = { ...result, performance: [hotDynamic] }; + } // Contract types (@scriptc/types): written annotations, checked against // the IR by the compile pipeline (contracts/check.ts). // Acyclic object types name the IR unit the program lowered them to; a diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index a1f8601ccb..92ee6b75eb 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -157,6 +157,7 @@ import { type EarlyExecutableNativeFeatures, } from "./executable/executable-cache.js"; import { compilerImplementationIdentity } from "./library/compiler-self-identity.js"; +import { isPerformanceCode } from "./diagnostics/dynamic-tier.js"; export const VERSION = "0.0.1"; @@ -454,6 +455,15 @@ interface CompileWarningSink { sourceTexts: Map | null; } +/** The build's performance warnings (SC61xx), stored with its cache entry + * so a cache hit repeats them. */ +function performanceWarnings(sink: CompileWarningSink | undefined): { + performance?: ScrDiagnostic[]; +} { + const performance = (sink?.warnings ?? []).filter((d) => isPerformanceCode(d.code)); + return performance.length === 0 ? {} : { performance }; +} + function withCompileWarnings( result: T, sink: CompileWarningSink, @@ -1406,6 +1416,12 @@ async function compileTracked( if (earlyCacheOptions === null) { throw new InternalCompilerError("executable cache hit without executable cache options"); } + // A cached build repeats its performance warning (SC6101): the code it + // names is just as untyped as when it was built. + if (warningSink !== undefined && earlyHit.performance !== undefined) { + warningSink.warnings = earlyHit.performance; + warningSink.sourceTexts = new Map(); + } const executableCacheOptions = earlyCacheOptions; // Route/proof metadata is independently evictable. A full-compiler // fallback that still finds the validated payload repairs that lightweight @@ -1595,6 +1611,7 @@ async function compileTracked( nativeDependencies: dependencies, frontend: frontendInputs.snapshot(), ...(irPath === undefined ? {} : { irPath }), + ...performanceWarnings(warningSink), }); publishedExecutable = true; }, @@ -1636,6 +1653,7 @@ async function compileTracked( executableRestored: false, frontend: frontendInputs.snapshot(), ...(irPath === undefined ? {} : { irPath }), + ...performanceWarnings(warningSink), }).catch(() => undefined); } await pruneBuildCache(cacheRoot); diff --git a/packages/compiler/src/startup-cache.ts b/packages/compiler/src/startup-cache.ts index c2f82fdad6..1bc74bef05 100644 --- a/packages/compiler/src/startup-cache.ts +++ b/packages/compiler/src/startup-cache.ts @@ -25,3 +25,4 @@ export function precompiledRuntimePackTarget( export { checkerCacheIdentity } from "./frontend/ts7/checker-backend.js"; export { readCoverageVerdict } from "./coverage/verdict-startup.js"; export { coverageEnvelope, coveragePasses, renderCoverage } from "./coverage/report.js"; +export { renderPerformanceWarnings } from "./diagnostics/dynamic-tier.js"; diff --git a/tests/coverage-fixtures/dynamic-tier/hot.js b/tests/coverage-fixtures/dynamic-tier/hot.js new file mode 100644 index 0000000000..af2afac07d --- /dev/null +++ b/tests/coverage-fixtures/dynamic-tier/hot.js @@ -0,0 +1,36 @@ +// Untyped JavaScript whose functions loop over their arguments: the build +// names them (SC6101). `scale` runs once and stays quiet. +function scale(points, factor) { + return { points, factor }; +} + +function centroid(points) { + let x = 0; + let y = 0; + for (const p of points) { + x += p.x; + y += p.y; + } + return { x: x / points.length, y: y / points.length }; +} + +function distance(a, b) { + const dx = a.x - b.x; + const dy = a.y - b.y; + return Math.sqrt(dx * dx + dy * dy); +} + +function spread(points) { + const c = centroid(points); + let total = 0; + for (let i = 0; i < points.length; i++) total += distance(points[i], c); + return total; +} + +const points = [ + { x: 0, y: 0 }, + { x: 4, y: 0 }, + { x: 0, y: 3 }, +]; +const scaled = scale(points, 2); +console.log(scaled.factor, spread(points).toFixed(3)); diff --git a/tests/coverage-fixtures/dynamic-tier/package/main.ts b/tests/coverage-fixtures/dynamic-tier/package/main.ts new file mode 100644 index 0000000000..3acd390da3 --- /dev/null +++ b/tests/coverage-fixtures/dynamic-tier/package/main.ts @@ -0,0 +1,13 @@ +// A program whose own hot function uses an `any` value, and which calls an +// untyped npm package: the warning names the function and summarizes the +// package's dynamic-tier code. +import { tally } from "untyped-lib"; + +function sumAmounts(rows: any): number { + let total = 0; + for (const row of rows) total += row.amount; + return total; +} + +const rows = [{ amount: 2 }, { amount: 5 }]; +console.log(sumAmounts(rows), tally(["a", "b", "a"]).a); diff --git a/tests/coverage-fixtures/dynamic-tier/package/node_modules/untyped-lib/index.d.ts b/tests/coverage-fixtures/dynamic-tier/package/node_modules/untyped-lib/index.d.ts new file mode 100644 index 0000000000..c8871efd07 --- /dev/null +++ b/tests/coverage-fixtures/dynamic-tier/package/node_modules/untyped-lib/index.d.ts @@ -0,0 +1 @@ +export function tally(values: any): any; diff --git a/tests/coverage-fixtures/dynamic-tier/package/node_modules/untyped-lib/index.js b/tests/coverage-fixtures/dynamic-tier/package/node_modules/untyped-lib/index.js new file mode 100644 index 0000000000..5695b6df2e --- /dev/null +++ b/tests/coverage-fixtures/dynamic-tier/package/node_modules/untyped-lib/index.js @@ -0,0 +1,6 @@ +// Counts each value: untyped JavaScript in a package. +exports.tally = function tally(values) { + const counts = {}; + for (const v of values) counts[v] = (counts[v] || 0) + 1; + return counts; +}; diff --git a/tests/coverage-fixtures/dynamic-tier/package/node_modules/untyped-lib/package.json b/tests/coverage-fixtures/dynamic-tier/package/node_modules/untyped-lib/package.json new file mode 100644 index 0000000000..a81ea84d46 --- /dev/null +++ b/tests/coverage-fixtures/dynamic-tier/package/node_modules/untyped-lib/package.json @@ -0,0 +1 @@ +{"name": "untyped-lib", "version": "1.0.0", "main": "index.js", "types": "index.d.ts"} diff --git a/tests/coverage-fixtures/dynamic-tier/quiet.ts b/tests/coverage-fixtures/dynamic-tier/quiet.ts new file mode 100644 index 0000000000..d045a9392d --- /dev/null +++ b/tests/coverage-fixtures/dynamic-tier/quiet.ts @@ -0,0 +1,13 @@ +// An untyped configuration read once at startup: dynamic, but not hot, so +// the build names nothing. The hot loop is typed. +const config: any = JSON.parse('{"scale": 3, "label": "sum"}'); +const scale: number = config.scale; +const label: string = config.label; + +function sum(values: number[]): number { + let total = 0; + for (const v of values) total += v * scale; + return total; +} + +console.log(label, sum([1, 2, 3])); diff --git a/tests/coverage-fixtures/dynamic-tier/typed-hot.ts b/tests/coverage-fixtures/dynamic-tier/typed-hot.ts new file mode 100644 index 0000000000..e72a7ca7ad --- /dev/null +++ b/tests/coverage-fixtures/dynamic-tier/typed-hot.ts @@ -0,0 +1,21 @@ +// TypeScript functions that loop over untyped values: an `any` parameter +// and an untyped JSON.parse result (SC6101 names both). +function regionTotals(items: any): Map { + const out = new Map(); + for (const item of items) out.set(item.region, (out.get(item.region) ?? 0) + item.amount); + return out; +} + +function countRegions(text: string): number { + const parsed = JSON.parse(text); + let n = 0; + for (let i = 0; i < parsed.length; i++) if (parsed[i].region !== "") n++; + return n; +} + +const rows = [ + { amount: 3, region: "eu" }, + { amount: 4, region: "us" }, +]; +for (const [region, total] of regionTotals(rows)) console.log(region, total); +console.log(countRegions(JSON.stringify(rows))); diff --git a/tests/harness/__snapshots__/coverage-dynamic-tier.txt b/tests/harness/__snapshots__/coverage-dynamic-tier.txt new file mode 100644 index 0000000000..8072021e35 --- /dev/null +++ b/tests/harness/__snapshots__/coverage-dynamic-tier.txt @@ -0,0 +1,13 @@ +scriptc coverage tests/coverage-fixtures/dynamic-tier/hot.js + + statements analyzed 18 + compile statically 18 (100%) + in dynamic tier 14 (operate on values typed only at runtime; --explain-dynamic lists them) + + fully static — all analyzed statements compile to native code. + + untyped and likely hot 3 functions that likely run often use untyped values, which runs slower than typed code: + centroid (hot.js:7): untyped JavaScript file, in a loop + spread (hot.js:23): untyped JavaScript file, in a loop + distance (hot.js:17): untyped JavaScript file, called in a loop + hint: add types to compile these statically; see scriptc coverage --explain-dynamic \ No newline at end of file diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index cc91c09223..ca62352b11 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -113,6 +113,48 @@ test("ordinary code reports no divergence", () => { expect(coverage.divergences ?? []).toEqual([]); }); +test("likely-hot functions with untyped values report as a performance warning", async () => { + const { coverage } = analyze(fixture("dynamic-tier/hot.js")); + expect(coverage.diagnostics).toEqual([]); + expect(coverage.divergences ?? []).toEqual([]); + expect((coverage.performance ?? []).map((d) => d.code)).toEqual(["SC6101"]); + // Not a blocker or a divergence: only --fail-on=dynamic fails on it. + expect(coveragePasses(coverage, "divergences")).toBe(true); + expect(coveragePasses(coverage, "dynamic")).toBe(false); + const envelope = coverageEnvelope(coverage, { compilerVersion: "test" }); + expect(envelope.diagnostics.map((d) => [d.code, d.category, d.severity])).toEqual([ + ["SC6101", "performance", "warning"], + ]); + await expect(report(fixture("dynamic-tier/hot.js"))).toMatchFileSnapshot( + "__snapshots__/coverage-dynamic-tier.txt", + ); +}); + +test("the performance warning names the untyped value of each function", () => { + const { coverage } = analyze(fixture("dynamic-tier/typed-hot.ts")); + expect(coverage.performance?.[0]?.message.split("\n")).toEqual([ + "2 functions that likely run often use untyped values, which runs slower than typed code:", + " regionTotals (typed-hot.ts:3): `items` is `any`, in a loop", + " countRegions (typed-hot.ts:9): `parsed` holds an untyped `JSON.parse` result, in a loop", + ]); +}); + +test("untyped setup code that runs once reports no performance warning", () => { + const { coverage } = analyze(fixture("dynamic-tier/quiet.ts")); + expect(coverage.diagnostics).toEqual([]); + expect(coverage.performance).toBeUndefined(); + expect(coveragePasses(coverage, "dynamic")).toBe(true); +}); + +test("dynamic-tier code in npm packages is summarized by package", () => { + const { coverage } = analyze(fixture("dynamic-tier/package/main.ts")); + expect(coverage.performance?.[0]?.message.split("\n")).toEqual([ + "1 function that likely runs often uses untyped values, which runs slower than typed code:", + " sumAmounts (main.ts:6): `rows` is `any`, in a loop", + " untyped code in npm packages: untyped-lib (1 function)", + ]); +}); + test("statements of a signature-blocked function count as not static", () => { const { coverage } = analyze(fixture("blocked-signature.ts")); const un = coverage.unreached?.stats; From 5a3fbcc1ef74eebdb4118e638f6df0d72c6c9fa6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 18:57:17 -0500 Subject: [PATCH 015/395] Hold class values in structural constructor types declared in the program A constructor type whose instances are not one program class's, such as `new () => unknown`, a type that also names static members (`{ readonly schema: unknown; new (): unknown }`), an abstract constructor type over a record, or construct overloads, did not map and refused with SC2001. Such a type declared in the program now lowers as a checked-dynamic value, as an `unknown` slot already holds a class value: the value keeps its class identity, `new` through it constructs that class, static reads see that class's statics, and the constructed value checks into the signature's instance type. Library and package constructor types keep their fences. Refs #1372 Co-authored-by: blurname <50226652+blurname@users.noreply.github.com> --- packages/compiler/src/frontend/type-mapper.ts | 16 +++++ .../6911-structural-constructor-types.ts.json | 6 ++ .../6911-structural-constructor-types.ts | 59 +++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6911-structural-constructor-types.ts.json create mode 100644 tests/corpus/6911-structural-constructor-types.ts diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 3448545f8c..b0e3a6dc96 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -2459,6 +2459,22 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return { kind: "classval", className: inst.className }; } } + // A constructable declared in the program whose instances are not one + // program class's (`new () => unknown`, `abstract new () => S` over a + // record, a type that also names static members such as `{ readonly + // schema: unknown; new (): unknown }`, construct overloads): the class + // value rides the checked-dynamic tree, as an `unknown` slot already + // holds one. `new` through it and static reads dispatch at run time, + // and the constructed value checks into the signature's instance type. + // Lib and package constructables keep their fences. + if ( + ctorSigs.length > 0 && + checker.getCallSignatures(widened).length === 0 && + (widened.getSymbol()?.declarations.length ?? 0) > 0 && + widened.getSymbol()!.declarations.every((d) => ctx.isProgramFile(ts.getSourceFileOfNode(d))) + ) { + return DYN; + } } // Map: a reference to the standard library's Map interface — // provenance, not the name (a user's own `interface Map` maps as a record diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6911-structural-constructor-types.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6911-structural-constructor-types.ts.json new file mode 100644 index 0000000000..3d14386873 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6911-structural-constructor-types.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6911-structural-constructor-types.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6911-structural-constructor-types.ts b/tests/corpus/6911-structural-constructor-types.ts new file mode 100644 index 0000000000..e8df74dfad --- /dev/null +++ b/tests/corpus/6911-structural-constructor-types.ts @@ -0,0 +1,59 @@ +// Values typed by a structural constructor type: `new () => unknown`, a type +// that also names static members, abstract constructor types and arrays of +// constructors. The class value keeps its identity; `new` through it builds +// an instance of the class it holds, and static reads see that class's +// statics. +type DescribedClass = { readonly schema: unknown; new (): unknown }; +class Item { + static readonly schema = { name: true }; + value = 42; +} +class Other { + static readonly schema = { name: false, extra: 1 }; + value = 7; +} +function classOf(flag: boolean): DescribedClass { + return flag ? Item : Other; +} +console.log(JSON.stringify(classOf(true).schema), JSON.stringify(classOf(false).schema)); +const C = classOf(true); +const made = new C(); +console.log(made instanceof Item, (made as Item).value, C === Item); + +type Ctor = new () => unknown; +const ctors: Ctor[] = [Item, Other]; +console.log(ctors.length, ctors.map((c) => (new c() as { value: number }).value).join(",")); + +class Point { + readonly x: number; + readonly y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + static origin = "0,0"; +} +type PointLike = { readonly origin: string; new (x: number, y: number): { x: number; y: number } }; +const P: PointLike = Point; +const p = new P(3, 4); +console.log(p.x + p.y, P.origin); + +abstract class Shape { + abstract area(): number; + describe(): string { + return `area ${this.area()}`; + } +} +class Square extends Shape { + area(): number { + return 4; + } +} +type ShapeCtor = abstract new () => Shape; +const sc: ShapeCtor = Square; +console.log(sc === Square, new (sc as new () => Shape)().describe()); + +function make(c: new () => T): T { + return new c(); +} +console.log((make(Item) as Item).value, (make(Other) as Other).value); From 44046ba579dc53f4bb788fef12eee7912b0148ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 21 Oct 2026 21:02:15 -0300 Subject: [PATCH 016/395] Destructure any sources read through string-literal keys --- .../src/frontend/lowering/lower-stmts.ts | 5 +- .../any-destructuring-string-rhs.ts.json | 6 ++ .../any-destructuring-rhs-spelling.ts.json | 6 ++ tests/corpus/any-destructuring-string-rhs.ts | 72 +++++++++++++++++++ .../any-destructuring-rhs-spelling.ts | 8 +++ .../any-destructuring-rhs-spelling.ts.txt | 29 ++++++++ .../any-destructuring-string-rhs.ts.json | 1 + 7 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/any-destructuring-string-rhs.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/any-destructuring-rhs-spelling.ts.json create mode 100644 tests/corpus/any-destructuring-string-rhs.ts create mode 100644 tests/diagnostics/any-destructuring-rhs-spelling.ts create mode 100644 tests/harness/__snapshots__/any-destructuring-rhs-spelling.ts.txt create mode 100644 tests/harness/corpus-costs/any-destructuring-string-rhs.ts.json diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index b04ba0b098..391dcbff07 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -8212,7 +8212,7 @@ export function lowerDestructuringAssign( /** V8's spelling of a destructuring RHS in its TypeError message * ("Cannot destructure 'a' as it is undefined.") — plain identifiers * spell their name (parens unwrap), `this`, property chains and - * identifier or numeric element reads spell their source, awaited and + * identifier, numeric or string element reads spell their source, awaited and * yielded values spell "(intermediate value)", and calls append an * ellipsis to a supported callee spelling. Other CallPrinter forms keep * a fence. */ @@ -8245,6 +8245,9 @@ function destrSpellingOf(rhs: ts.Expression): string | null { if (base === null) return null; if (ts.isIdentifier(key)) return `${base}[${key.text}]`; if (ts.isNumericLiteral(key)) return `${base}[${String(Number(key.text))}]`; + // V8 prints string keys as a property read, using their decoded text, + // even when the key is not an identifier (e.g. o["a.b"] reads "o.a.b"). + if (ts.isStringLiteral(key)) return `${base}.${key.text}`; return null; } if (ts.isCallExpression(e) && !e.questionDotToken) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/any-destructuring-string-rhs.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/any-destructuring-string-rhs.ts.json new file mode 100644 index 0000000000..fbea11f4a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/any-destructuring-string-rhs.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/any-destructuring-string-rhs.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/any-destructuring-rhs-spelling.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/any-destructuring-rhs-spelling.ts.json new file mode 100644 index 0000000000..bb815d0699 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/any-destructuring-rhs-spelling.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/any-destructuring-rhs-spelling.ts" + ], + "diags": [] +} diff --git a/tests/corpus/any-destructuring-string-rhs.ts b/tests/corpus/any-destructuring-string-rhs.ts new file mode 100644 index 0000000000..ba908883d7 --- /dev/null +++ b/tests/corpus/any-destructuring-string-rhs.ts @@ -0,0 +1,72 @@ +// String-literal RHS reads share the existing checked-dynamic destructuring path. +const events: any = { + "a.b": { value: 1, nested: { count: 2 }, list: [3, 4, 5], extra: 6 }, + "a\nb": { value: 7 }, + 'a"b': { value: 8 }, + "a\\b": { value: 9 }, + "": { value: 10 }, +}; +const { value } = events["a.b"]; +console.log(value); +const { value: renamed, missing = 11, nested: { count }, ...rest } = events['a.b']; +console.log(renamed, missing, count, JSON.stringify(rest)); +const { list: [head, absent = 12, ...tail] } = events["a.b"]; +console.log(head, absent, tail); +const { value: newline } = events["a\nb"]; +const { value: quote } = events['a"b']; +const { value: slash } = events["a\\b"]; +const { value: unicode } = events["\u0061.b"]; +const { value: empty } = events[""]; +console.log(newline, quote, slash, unicode, empty); + +let calls = 0; +let defaults = 0; +function source(): any { + calls++; + console.log("source", calls); + return events; +} +function fallback(): number { + defaults++; + console.log("default", defaults); + return 13; +} +const { value: found = fallback(), missing: filled = fallback(), extra } = source()["a.b"]; +console.log(found, filled, extra, calls, defaults); + +// Read the RHS once, then evaluate getters and lazy defaults in binding order. +const observed: any = {}; +Object.defineProperty(observed, "first", { get() { console.log("get first"); return 14; } }); +Object.defineProperty(observed, "missing", { get() { console.log("get missing"); return undefined; } }); +Object.defineProperty(observed, "last", { get() { console.log("get last"); return 15; } }); +const outer: any = {}; +Object.defineProperty(outer, "payload", { get() { console.log("get payload"); return observed; } }); +const { first: observedFirst, missing: observedDefault = fallback(), last: observedLast } = outer["payload"]; +console.log(observedFirst, observedDefault, observedLast, defaults); + +// Assignment uses the same source spelling helper as declarations. +let assigned: any; +({ value: assigned } = events["a.b"]); +console.log("assigned", assigned); + +// A reduced event dispatcher consumer: nested payloads from an untyped table. +function dispatch(table: any): void { + const { nested: { count = 0 }, list: [first, ...remaining] } = table["a.b"]; + console.log("dispatch", count, first, remaining); +} +dispatch(events); + +// Error spelling uses decoded string text, including punctuation and escapes. +const absentSource: any = {}; +for (const attempt of [ + () => { const { value } = absentSource["a.b"]; console.log(value); }, + () => { const { value } = absentSource["a\nb"]; console.log(value); }, + () => { const { value } = absentSource['a"b']; console.log(value); }, + () => { const { value } = absentSource["a\\b"]; console.log(value); }, + () => { const { value } = absentSource["\u0061"]; console.log(value); }, + () => { const {} = absentSource[""]; }, + () => { ({ value: assigned } = absentSource["a.b"]); }, + () => { const { value } = source()["not.found"]; console.log(value); }, +]) { + try { attempt(); } catch (e) { console.log(e instanceof TypeError, (e as Error).message); } +} diff --git a/tests/diagnostics/any-destructuring-rhs-spelling.ts b/tests/diagnostics/any-destructuring-rhs-spelling.ts new file mode 100644 index 0000000000..201b579b41 --- /dev/null +++ b/tests/diagnostics/any-destructuring-rhs-spelling.ts @@ -0,0 +1,8 @@ +// String-literal RHS keys are supported; other CallPrinter forms keep their fences. +function unsupported(table: any, key: string): void { + const { first } = table[key + "suffix"]; + const { second } = table?.["a.b"]; + const { third } = table[`a.b`]; + console.log(first, second, third); +} +unsupported({}, "a"); diff --git a/tests/harness/__snapshots__/any-destructuring-rhs-spelling.ts.txt b/tests/harness/__snapshots__/any-destructuring-rhs-spelling.ts.txt new file mode 100644 index 0000000000..a5116f1f9c --- /dev/null +++ b/tests/harness/__snapshots__/any-destructuring-rhs-spelling.ts.txt @@ -0,0 +1,29 @@ +any-destructuring-rhs-spelling.ts:3:21 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet + + 2 | function unsupported(table: any, key: string): void { + 3 | const { first } = table[key + "suffix"]; + | ^~~~~~~~~~~~~~~~~~~~~ + 4 | const { second } = table?.["a.b"]; + +any-destructuring-rhs-spelling.ts:4:22 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet + + 3 | const { first } = table[key + "suffix"]; + 4 | const { second } = table?.["a.b"]; + | ^~~~~~~~~~~~~~ + 5 | const { third } = table[`a.b`]; + +any-destructuring-rhs-spelling.ts:5:21 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet + + 4 | const { second } = table?.["a.b"]; + 5 | const { third } = table[`a.b`]; + | ^~~~~~~~~~~~ + 6 | console.log(first, second, third); + +any-destructuring-rhs-spelling.ts:6:15 - error SC2004: uses of 'first' inherit the blocker on its declaration + + 5 | const { third } = table[`a.b`]; + 6 | console.log(first, second, third); + | ^~~~~ + 7 | } + + hint: the declaration of 'first' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file diff --git a/tests/harness/corpus-costs/any-destructuring-string-rhs.ts.json b/tests/harness/corpus-costs/any-destructuring-string-rhs.ts.json new file mode 100644 index 0000000000..5387823ba8 --- /dev/null +++ b/tests/harness/corpus-costs/any-destructuring-string-rhs.ts.json @@ -0,0 +1 @@ +{ "san": 4048 } From be23f71266e2482b5ce75d47be896c36cf54c2b0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 19:02:52 -0500 Subject: [PATCH 017/395] Compile TypeScript's UMD module wrapper as the module body --- .../src/frontend/npm-static-umd.test.ts | 109 +++++++++++ .../compiler/src/frontend/npm-static-umd.ts | 178 ++++++++++++++++++ packages/compiler/src/frontend/npm-static.ts | 5 + packages/compiler/src/frontend/services.ts | 8 + .../7000-umd-package-entry/main.ts.json | 6 + tests/corpus/7000-umd-package-entry/main.ts | 10 + .../umd-shapes/lib/impl/measure.js | 22 +++ .../node_modules/umd-shapes/lib/main.d.ts | 12 ++ .../node_modules/umd-shapes/lib/main.js | 29 +++ .../node_modules/umd-shapes/package.json | 1 + .../7000-umd-package-entry/main.ts.json | 1 + 11 files changed, 381 insertions(+) create mode 100644 packages/compiler/src/frontend/npm-static-umd.test.ts create mode 100644 packages/compiler/src/frontend/npm-static-umd.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7000-umd-package-entry/main.ts.json create mode 100644 tests/corpus/7000-umd-package-entry/main.ts create mode 100644 tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/lib/impl/measure.js create mode 100644 tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/lib/main.d.ts create mode 100644 tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/lib/main.js create mode 100644 tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/package.json create mode 100644 tests/harness/corpus-costs/7000-umd-package-entry/main.ts.json diff --git a/packages/compiler/src/frontend/npm-static-umd.test.ts b/packages/compiler/src/frontend/npm-static-umd.test.ts new file mode 100644 index 0000000000..5280ecb179 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-umd.test.ts @@ -0,0 +1,109 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, expect, test } from "vitest"; +import { nodeFrontendServices } from "./services-node.js"; + +const dirs: string[] = []; +afterAll(() => { + for (const dir of dirs) rmSync(dir, { recursive: true, force: true }); +}); + +function unwrap(source: string): string | null { + return nodeFrontendServices().umdFactory(source, "/pkg/index.js"); +} + +/** TypeScript's UMD wrapper around `body`. */ +function umd(body: string, params = "require, exports"): string { + return `(function (factory) { + if (typeof module === "object" && typeof module.exports === "object") { + var v = factory(require, exports); + if (v !== undefined) module.exports = v; + } + else if (typeof define === "function" && define.amd) { + define(["require", "exports"], factory); + } +})(function (${params}) { +${body} +}); +`; +} + +const probe = ` +console.log(JSON.stringify(module.exports), Object.keys(module.exports).join(",")); +`; + +function run(file: string): string { + const result = spawnSync(process.execPath, [file], { encoding: "utf8" }); + expect(result.status).toBe(0); + return result.stdout; +} + +/** The unwrapped module exports what the original exports. */ +function expectSameModule(source: string): string { + const result = unwrap(source); + expect(typeof result).toBe("string"); + const dir = mkdtempSync(join(tmpdir(), "scriptc-umd-")); + dirs.push(dir); + const original = join(dir, "original.cjs"); + const unwrapped = join(dir, "unwrapped.cjs"); + writeFileSync(original, source + probe); + writeFileSync(unwrapped, result! + probe); + const answer = run(original); + expect(run(unwrapped)).toBe(answer); + return result!; +} + +test("the factory body runs at the top level with its positions kept", () => { + const source = umd(` "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.twice = twice; + const path = require("path"); + exports.sep = path.sep; + function twice(n) { + return n * 2; + }`); + const result = expectSameModule(source); + expect(result.length).toBe(source.length); + expect(result.split("\n").length).toBe(source.split("\n").length); + expect(result.indexOf("exports.twice = twice;")).toBe(source.indexOf("exports.twice = twice;")); + expect(result).not.toContain("define.amd"); + expect(result).not.toContain("factory"); +}); + +test("TypeScript's helper probes of this keep their fallback", () => { + expectSameModule( + umd(` "use strict"; + var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; + }; + Object.defineProperty(exports, "__esModule", { value: true }); + const os_1 = __importDefault(require("os")); + exports.eol = os_1.default.EOL;`), + ); +}); + +test("nested functions keep their own return, arguments and this", () => { + expectSameModule( + umd(` "use strict"; + exports.first = function () { return arguments[0]; }; + exports.self = { get() { return this; } }; + exports.arrow = () => { return 1; };`), + ); +}); + +test("other shapes keep the original text", () => { + // `export =` returns the module value from the factory. + expect(unwrap(umd(` "use strict";\n return { a: 1 };`))).toBeNull(); + // The body's own `this` and `arguments` differ from the module's. + expect(unwrap(umd(` exports.self = this;`))).toBeNull(); + expect(unwrap(umd(` exports.count = arguments.length;`))).toBeNull(); + // Other factory parameters. + expect(unwrap(umd(` exports.a = 1;`, "require, exports, module"))).toBeNull(); + expect(unwrap(umd(` exports.a = 1;`, "req, exp"))).toBeNull(); + // Code after the wrapper. + expect(unwrap(umd(` exports.a = 1;`) + "exports.b = 2;\n")).toBeNull(); + // Not a UMD wrapper. + expect(unwrap(`exports.a = 1;\n`)).toBeNull(); +}); diff --git a/packages/compiler/src/frontend/npm-static-umd.ts b/packages/compiler/src/frontend/npm-static-umd.ts new file mode 100644 index 0000000000..88400e0464 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-umd.ts @@ -0,0 +1,178 @@ +/* TypeScript's UMD module emit in a compiled package: the module body is a + * factory function that the wrapper calls with the CommonJS module's own + * `require` and `exports`. + * + * (function (factory) { + * if (typeof module === "object" && typeof module.exports === "object") { + * var v = factory(require, exports); + * if (v !== undefined) module.exports = v; + * } + * else if (typeof define === "function" && define.amd) { + * define(["require", "exports", "./dep"], factory); + * } + * })(function (require, exports) { + * "use strict"; + * const dep = require("./dep"); + * exports.f = f; + * }); + * + * Under Node the CommonJS branch runs: the factory body runs once with the + * module's `require` and `exports`, and a body without a `return` leaves + * `module.exports` as it was. That is the module's own body, so the wrapper + * is blanked out (line breaks kept, so positions stay) and the body runs at + * the module's top level, where its requires and export assignments are the + * module's own. + * + * Accepted: the wrapper is the file's only statement, its CommonJS branch + * calls the factory as `factory(require, exports)`, the factory's + * parameters are exactly `require` and `exports`, and the body uses no + * `return` or `arguments` of its own. `this` in the body is the factory's + * receiver, not the module's: it may appear only in TypeScript's helper + * probes (`this && this.__importDefault`), which find no helper on either + * receiver. Any other shape keeps the original text. */ +import * as ts from "./ts7/syntax.js"; +import { tokenStartOfNode } from "./ts7/node-functions.js"; +import { applyEdits } from "./npm-static-bundled-chunks.js"; + +/** True when the text may hold a UMD wrapper (a cheap probe). */ +export function mayHoldUmdWrapper(source: string): boolean { + return source.includes("define.amd") && source.includes("(require, exports)"); +} + +/** The module text with a UMD wrapper's factory body at the top level, or + * null when the file is not one. */ +export function unwrapUmdFactory(sf: ts.SourceFile): string | null { + const stmts = sf.statements.nodes; + if (stmts.length !== 1) return null; + const stmt = stmts[0]!; + if (!ts.isExpressionStatement(stmt) || !ts.isCallExpression(stmt.expression)) return null; + const call = stmt.expression; + if (call.arguments.nodes.length !== 1) return null; + const wrapper = unparenthesized(call.expression); + const factory = call.arguments.nodes[0]!; + if (!ts.isFunctionExpression(wrapper) || !ts.isFunctionExpression(factory)) return null; + const wrapperParams = wrapper.parameters.nodes; + if (wrapperParams.length !== 1 || !isPlainParameter(wrapperParams[0]!)) return null; + const factoryName = (wrapperParams[0]!.name as ts.Identifier).text; + if (!callsFactoryForCommonJs(wrapper, factoryName)) return null; + const params = factory.parameters.nodes; + if ( + params.length !== 2 || + !params.every(isPlainParameter) || + (params[0]!.name as ts.Identifier).text !== "require" || + (params[1]!.name as ts.Identifier).text !== "exports" + ) + return null; + const body = factory.body; + if (body === undefined || !ownBodyKeepsModuleSemantics(body)) return null; + const open = tokenStartOfNode(body) + 1; + const close = body.end - 1; + return applyEdits(sf.text, 0, sf.text.length, [ + { start: tokenStartOfNode(stmt), end: open, text: "" }, + { start: close, end: stmt.end, text: "" }, + ]); +} + +function unparenthesized(e: ts.Expression): ts.Expression { + while (ts.isParenthesizedExpression(e)) e = e.expression; + return e; +} + +/** A parameter that is a bare identifier: no default, rest or pattern. */ +function isPlainParameter(p: ts.ParameterDeclaration): boolean { + return ts.isIdentifier(p.name) && p.initializer === undefined && p.dotDotDotToken === undefined; +} + +/** True when the wrapper's first statement is TypeScript's CommonJS test, + * `if (typeof module === "object" && typeof module.exports === "object")`, + * whose branch starts by calling the factory as `factory(require, exports)`. */ +function callsFactoryForCommonJs(wrapper: ts.FunctionExpression, factoryName: string): boolean { + const first = wrapper.body?.statements.nodes[0]; + if (first === undefined || !ts.isIfStatement(first)) return false; + const sf = ts.getSourceFileOfNode(wrapper); + const condition = sf.text.slice(tokenStartOfNode(first.expression), first.expression.end); + if ( + condition.replace(/\s+/g, " ") !== + 'typeof module === "object" && typeof module.exports === "object"' + ) + return false; + const branch = first.thenStatement; + const call = ts.isBlock(branch) ? branch.statements.nodes[0] : undefined; + if (call === undefined || !ts.isVariableStatement(call)) return false; + const declarations = call.declarationList.declarations.nodes; + const init = declarations.length === 1 ? declarations[0]!.initializer : undefined; + if (init === undefined || !ts.isCallExpression(init)) return false; + const args = init.arguments.nodes; + return ( + ts.isIdentifier(init.expression) && + init.expression.text === factoryName && + args.length === 2 && + ts.isIdentifier(args[0]!) && + args[0].text === "require" && + ts.isIdentifier(args[1]!) && + args[1].text === "exports" + ); +} + +/** True when the factory body behaves the same as the module's top level: + * no `return` or `arguments` of its own, and `this` only in TypeScript's + * helper probes. */ +function ownBodyKeepsModuleSemantics(body: ts.Block): boolean { + let keeps = true; + ts.walkPreorder(body, (n) => { + if (!keeps) return "stop"; + // Nested functions have their own `return`, `arguments` and `this`; + // arrow functions only their own `return`. + if ( + ts.isFunctionDeclaration(n) || + ts.isFunctionExpression(n) || + ts.isMethodDeclaration(n) || + ts.isGetAccessorDeclaration(n) || + ts.isSetAccessorDeclaration(n) || + ts.isConstructorDeclaration(n) + ) + return "skip"; + if (ts.isClassDeclaration(n) || ts.isClassExpression(n)) return "skip"; + if (ts.isReturnStatement(n) && !insideArrow(n, body)) keeps = false; + else if (ts.isIdentifier(n) && n.text === "arguments") keeps = false; + else if (n.kind === ts.SyntaxKind.ThisKeyword && !isHelperProbe(n)) keeps = false; + return undefined; + }); + return keeps; +} + +/** True when `node` sits inside an arrow function nested in `body`. */ +function insideArrow(node: ts.Node, body: ts.Block): boolean { + for (let p = node.parent; p !== undefined && p !== body; p = p.parent) + if (ts.isArrowFunction(p)) return true; + return false; +} + +/** `this` in TypeScript's helper probe `this && this.__name`: the left + * operand, or the receiver of the right operand's `__` member. */ +function isHelperProbe(node: ts.Node): boolean { + const parent = node.parent; + if (parent === undefined) return false; + const probe = + ts.isBinaryExpression(parent) && parent.left === node + ? parent + : ts.isPropertyAccessExpression(parent) && + parent.expression === node && + ts.isIdentifier(parent.name) && + parent.name.text.startsWith("__") && + parent.parent !== undefined && + ts.isBinaryExpression(parent.parent) && + parent.parent.right === parent + ? parent.parent + : undefined; + if (probe === undefined || probe.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken) + return false; + const right = probe.right; + return ( + probe.left.kind === ts.SyntaxKind.ThisKeyword && + ts.isPropertyAccessExpression(right) && + right.expression.kind === ts.SyntaxKind.ThisKeyword && + ts.isIdentifier(right.name) && + right.name.text.startsWith("__") + ); +} diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index bc9fc2abde..56edafc48e 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -452,6 +452,10 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow } else if (esm !== null) { original = esm; } + // TypeScript's UMD wrapper calls its factory with the module's own + // require and exports: the factory body is the module body. + const umd = original === null ? null : services.umdFactory(original, path); + if (umd !== null) original = umd; // Polyfill packages that ARE a Node builtin compile as the builtin. const aliased = original === null ? null : services.builtinAliasRequires(original, path); if (aliased !== null) original = aliased; @@ -472,6 +476,7 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow if (exportsVar !== null) original = exportsVar; const respelled = split !== null || + umd !== null || tscHelpers !== null || esm !== null || aliased !== null || diff --git a/packages/compiler/src/frontend/services.ts b/packages/compiler/src/frontend/services.ts index 10931ab297..19728d6ce8 100644 --- a/packages/compiler/src/frontend/services.ts +++ b/packages/compiler/src/frontend/services.ts @@ -12,6 +12,7 @@ import { mayRequireBuiltinAlias, rewriteBuiltinAliasRequires, } from "./npm-static-builtin-aliases.js"; +import { mayHoldUmdWrapper, unwrapUmdFactory } from "./npm-static-umd.js"; import { mayHoldEnumIife, rewriteEnumIifes } from "./npm-static-enum-iife.js"; import { mayHoldTscBuiltinImport, unwrapTscBuiltinImports } from "./npm-static-tsc-helpers.js"; import { builtinPackageAliasFor } from "./package-shims.js"; @@ -114,6 +115,13 @@ export class FrontendServices { if (!mayHoldTscBuiltinImport(source)) return null; return unwrapTscBuiltinImports(this.parse(path, source, "js")); } + /** A package module written as TypeScript's UMD wrapper with its + * factory body at the top level (null: not one). */ + umdFactory(source: string, path: string): string | null { + this.ensureOpen(); + if (!mayHoldUmdWrapper(source)) return null; + return unwrapUmdFactory(this.parse(path, source, "js")); + } /** A package module with its literal-valued TypeScript enum IIFEs * respelled as object literals (null: none). */ enumIifes(source: string, path: string): string | null { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7000-umd-package-entry/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7000-umd-package-entry/main.ts.json new file mode 100644 index 0000000000..0c6cbd9d10 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7000-umd-package-entry/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7000-umd-package-entry/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7000-umd-package-entry/main.ts b/tests/corpus/7000-umd-package-entry/main.ts new file mode 100644 index 0000000000..4b8b3e2467 --- /dev/null +++ b/tests/corpus/7000-umd-package-entry/main.ts @@ -0,0 +1,10 @@ +// A package whose main entry is TypeScript's UMD emit: each module's body is +// a factory the wrapper calls with the module's own require and exports, so +// its requires and export assignments sit inside a function. Node runs the +// CommonJS branch, and the named imports read the factory's exports. +import { Shape, area, describe, label, unitCount } from "umd-shapes"; + +console.log(label, unitCount); +console.log(area({ kind: Shape.Square, size: 3 }), area({ kind: Shape.Circle, size: 1 }).toFixed(4)); +console.log(describe({ kind: Shape.Square, size: 2 })); +console.log(Shape.Circle, Shape[Shape.Square]); diff --git a/tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/lib/impl/measure.js b/tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/lib/impl/measure.js new file mode 100644 index 0000000000..0ea89b34e4 --- /dev/null +++ b/tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/lib/impl/measure.js @@ -0,0 +1,22 @@ +(function (factory) { + if (typeof module === "object" && typeof module.exports === "object") { + var v = factory(require, exports); + if (v !== undefined) module.exports = v; + } + else if (typeof define === "function" && define.amd) { + define(["require", "exports"], factory); + } +})(function (require, exports) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.units = void 0; + exports.square = square; + exports.circle = circle; + exports.units = ["cm", "m"]; + function square(size) { + return size * size; + } + function circle(radius) { + return Math.PI * radius * radius; + } +}); diff --git a/tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/lib/main.d.ts b/tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/lib/main.d.ts new file mode 100644 index 0000000000..0982da9ef7 --- /dev/null +++ b/tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/lib/main.d.ts @@ -0,0 +1,12 @@ +export declare enum Shape { + Square = 0, + Circle = 1 +} +export interface Figure { + kind: Shape; + size: number; +} +export declare const label: string; +export declare const unitCount: number; +export declare function area(figure: Figure): number; +export declare function describe(figure: Figure): string; diff --git a/tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/lib/main.js b/tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/lib/main.js new file mode 100644 index 0000000000..26162fbd92 --- /dev/null +++ b/tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/lib/main.js @@ -0,0 +1,29 @@ +(function (factory) { + if (typeof module === "object" && typeof module.exports === "object") { + var v = factory(require, exports); + if (v !== undefined) module.exports = v; + } + else if (typeof define === "function" && define.amd) { + define(["require", "exports", "./impl/measure"], factory); + } +})(function (require, exports) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.unitCount = exports.label = exports.Shape = void 0; + exports.area = area; + exports.describe = describe; + const measure = require("./impl/measure"); + var Shape; + (function (Shape) { + Shape[Shape["Square"] = 0] = "Square"; + Shape[Shape["Circle"] = 1] = "Circle"; + })(Shape || (exports.Shape = Shape = {})); + exports.label = "shapes"; + exports.unitCount = measure.units.length; + function area(figure) { + return figure.kind === Shape.Square ? measure.square(figure.size) : measure.circle(figure.size); + } + function describe(figure) { + return `${Shape[figure.kind]} of ${figure.size} ${measure.units[0]}: ${area(figure)}`; + } +}); diff --git a/tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/package.json b/tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/package.json new file mode 100644 index 0000000000..e827ecc417 --- /dev/null +++ b/tests/corpus/7000-umd-package-entry/node_modules/umd-shapes/package.json @@ -0,0 +1 @@ +{ "name": "umd-shapes", "version": "1.0.0", "main": "./lib/main.js", "types": "./lib/main.d.ts" } diff --git a/tests/harness/corpus-costs/7000-umd-package-entry/main.ts.json b/tests/harness/corpus-costs/7000-umd-package-entry/main.ts.json new file mode 100644 index 0000000000..782707a3aa --- /dev/null +++ b/tests/harness/corpus-costs/7000-umd-package-entry/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3911, "san": 3835 } From 7aff706f6c5d459c010f30c32c6447f132c5b80f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Wed, 21 Oct 2026 21:07:46 -0300 Subject: [PATCH 018/395] Preserve embedded NULs in destructuring source error spelling --- packages/compiler/src/backend/llvm/dyn.ts | 19 ++++++++++++++----- .../compiler/src/backend/llvm/expr-control.ts | 7 ++++++- tests/corpus/any-destructuring-string-rhs.ts | 6 +++++- .../any-destructuring-string-rhs.ts.json | 2 +- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 6911ef98b8..d05011d4e2 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -4128,11 +4128,12 @@ export class LlDyn { /* ── destructuring RequireObjectCoercible (ported) ─────────────────── */ - dynDestrCheckHelper(): string { - const memoKey = "%dynDestrCheck"; + dynDestrCheckHelper(spellingLength?: number): string { + const suffix = spellingLength === undefined ? "" : `_len${spellingLength}`; + const memoKey = `%dynDestrCheck${suffix}`; const existing = this.dynBuilders.get(memoKey); if (existing) return existing; - const name = "sc_dyn_destr_check"; + const name = `sc_dyn_destr_check${suffix}`; this.dynBuilders.set(memoKey, name); const host = this.host; host.declare(`declare void @scr_jb_init(ptr)`); @@ -4140,6 +4141,14 @@ export class LlDyn { host.declare(`declare void @scr_jb_puts(ptr, ptr)`); host.declare(`declare void @scr_throw_error(i32, ptr)`); const B = new BlockBuilder(); + const writeSpelling = (): void => { + if (spellingLength === undefined) { + B.line(`call void @scr_jb_puts(ptr %ddb, ptr %spell)`); + } else { + host.declare(`declare void @scr_jb_write_utf8(ptr, ptr, ${this.S})`); + B.line(`call void @scr_jb_write_utf8(ptr %ddb, ptr %spell, ${this.S} ${spellingLength})`); + } + }; const kd = this.kindOf(B, "%d"); const isU = B.tmp(); const isN = B.tmp(); @@ -4166,12 +4175,12 @@ export class LlDyn { this.puts(B, buf, "Cannot destructure property '"); B.line(`call void @scr_jb_puts(ptr ${buf}, ptr %firstProp)`); this.puts(B, buf, "' of '"); - B.line(`call void @scr_jb_puts(ptr ${buf}, ptr %spell)`); + writeSpelling(); this.puts(B, buf, "' as it is "); B.br(lTail); B.startBlock(lBare); this.puts(B, buf, "Cannot destructure '"); - B.line(`call void @scr_jb_puts(ptr ${buf}, ptr %spell)`); + writeSpelling(); this.puts(B, buf, "' as it is "); B.br(lTail); B.startBlock(lTail); diff --git a/packages/compiler/src/backend/llvm/expr-control.ts b/packages/compiler/src/backend/llvm/expr-control.ts index fd2061aaed..86962734aa 100644 --- a/packages/compiler/src/backend/llvm/expr-control.ts +++ b/packages/compiler/src/backend/llvm/expr-control.ts @@ -30,7 +30,12 @@ export function emitControlExpr( const v = host.emitExpr(e.value); const first = e.firstProp !== undefined ? host.cstr(e.firstProp) : "null"; - const helper = host.dyn.dynDestrCheckHelper(); + // Embedded NULs need a length-aware spelling writer. Keep the ordinary + // helper unchanged for existing source expressions. + const spellingLength = e.spelling.includes("\0") + ? Buffer.byteLength(e.spelling, "utf8") + : undefined; + const helper = host.dyn.dynDestrCheckHelper(spellingLength); B.line(`call void @${helper}(ptr ${v.name}, ptr ${host.cstr(e.spelling)}, ptr ${first})`); host.emitPendingCheck(); return v; diff --git a/tests/corpus/any-destructuring-string-rhs.ts b/tests/corpus/any-destructuring-string-rhs.ts index ba908883d7..217da07276 100644 --- a/tests/corpus/any-destructuring-string-rhs.ts +++ b/tests/corpus/any-destructuring-string-rhs.ts @@ -5,6 +5,7 @@ const events: any = { 'a"b': { value: 8 }, "a\\b": { value: 9 }, "": { value: 10 }, + "a\0b": { value: 16 }, }; const { value } = events["a.b"]; console.log(value); @@ -17,7 +18,8 @@ const { value: quote } = events['a"b']; const { value: slash } = events["a\\b"]; const { value: unicode } = events["\u0061.b"]; const { value: empty } = events[""]; -console.log(newline, quote, slash, unicode, empty); +const { value: nul } = events["a\0b"]; +console.log(newline, quote, slash, unicode, empty, nul); let calls = 0; let defaults = 0; @@ -65,6 +67,8 @@ for (const attempt of [ () => { const { value } = absentSource["a\\b"]; console.log(value); }, () => { const { value } = absentSource["\u0061"]; console.log(value); }, () => { const {} = absentSource[""]; }, + () => { const { value } = absentSource["a\0b"]; console.log(value); }, + () => { const {} = absentSource["a\0b"]; }, () => { ({ value: assigned } = absentSource["a.b"]); }, () => { const { value } = source()["not.found"]; console.log(value); }, ]) { diff --git a/tests/harness/corpus-costs/any-destructuring-string-rhs.ts.json b/tests/harness/corpus-costs/any-destructuring-string-rhs.ts.json index 5387823ba8..d9e939bce5 100644 --- a/tests/harness/corpus-costs/any-destructuring-string-rhs.ts.json +++ b/tests/harness/corpus-costs/any-destructuring-string-rhs.ts.json @@ -1 +1 @@ -{ "san": 4048 } +{ "san": 11627 } From 220199dfec1483bd7122c29de032f2f37aa59bd4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 19:17:07 -0500 Subject: [PATCH 019/395] Evaluate an fs argument inside satisfies once after a URL path The arguments after a URL path that are evaluated ahead of the conversion were keyed by the expression without its parentheses. When the fs lowering read through a satisfies wrapper, it lowered the inner expression again, so fs.chmodSync(url, mode() satisfies number) called mode() twice. The saved arguments are now keyed and looked up without their type-only wrappers (satisfies, as, type assertions, !), so each runs once. --- .../src/frontend/lowering/builtins/fs-paths.ts | 6 ++++-- packages/compiler/src/frontend/lowering/lowerer.ts | 8 ++++---- tests/corpus/6931-fs-url-paths.ts | 10 ++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts b/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts index 181e27e34a..38583025d1 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-paths.ts @@ -15,6 +15,7 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; import { sideEffectFreeOptionValue } from "../surfaces.js"; +import { stripTypeWrappers } from "../typed-arguments.js"; import { type IrExpr, type IrStmt, STRING, URL_T } from "../../../ir/ir.js"; /** The fs functions whose path parameters accept a URL: the path @@ -142,8 +143,9 @@ function checkUrlPaths( for (let i = first + 1; i < args.length; i++) if (!sideEffectFreeOptionValue(args[i]!)) last = i; const staged: ts.Expression[] = []; for (let i = first + 1; i <= last; i++) { - let e = args[i]!; - while (ts.isParenthesizedExpression(e)) e = e.expression; + // Keyed without type-only wrappers (`satisfies`, `as`, `!`), so a + // lowering that reads through them finds the saved value. + const e = stripTypeWrappers(args[i]!); // Literals are read by their members, so a whole-value slot cannot // hold them. if (ts.isObjectLiteralExpression(e) || ts.isArrayLiteralExpression(e)) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 80b2a59389..38d3ca8d3c 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,4 +1,4 @@ -import { onlyForwarded } from "./typed-arguments.js"; +import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js"; import { needsGeneralNodeStreams, noteUntypedStreamFlow, @@ -12046,11 +12046,11 @@ export class Lowerer { return this.stagedArgument(expr) ?? lowerExpr(this, expr); } - /** The saved value of a staged fs argument (stagedArguments), or null for - * every other expression. */ + /** The saved value of a staged fs argument (stagedArguments, keyed + * without type-only wrappers), or null for every other expression. */ private stagedArgument(node: ts.Expression): IrExpr | null { if (this.stagedArguments.size === 0) return null; - while (ts.isParenthesizedExpression(node)) node = node.expression; + node = stripTypeWrappers(node); if (!this.stagedArguments.has(node)) return null; const slot = this.stagedArguments.get(node); if (!slot) diff --git a/tests/corpus/6931-fs-url-paths.ts b/tests/corpus/6931-fs-url-paths.ts index cf68890669..effe64207f 100644 --- a/tests/corpus/6931-fs-url-paths.ts +++ b/tests/corpus/6931-fs-url-paths.ts @@ -103,6 +103,16 @@ console.log("uid", uid); uid = -1; attempt("chown order scheme", () => fs.chownSync(new URL("http://a/"), uid, bumpUid())); console.log("uid", uid); +// A later argument with effects inside a type-only wrapper runs once. +let modeCalls = 0; +function countedMode(): number { + modeCalls++; + return 0o600; +} +attempt("satisfies", () => fs.chmodSync(file, countedMode() satisfies number)); +attempt("as", () => fs.chmodSync(file, countedMode() as number)); +attempt("non-null", () => fs.chmodSync(file, countedMode()!)); +console.log("mode calls", modeCalls); fs.rmSync(dir, { recursive: true, force: true }); console.log("done", fs.existsSync(dir)); From 3447e9a1c13e6801ac6c3b9a455f70ed2f43d503 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 19:20:54 -0500 Subject: [PATCH 020/395] Unwrap a UMD wrapper only when its body is exactly TypeScript's emit The wrapper is dropped when its factory body moves to the top level, so any other statement in the wrapper, in its CommonJS branch or in its AMD branch would be lost. The check now matches the whole wrapper body and keeps the original text for any other shape. --- .../src/frontend/npm-static-umd.test.ts | 12 ++++ .../compiler/src/frontend/npm-static-umd.ts | 72 +++++++++++-------- 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/packages/compiler/src/frontend/npm-static-umd.test.ts b/packages/compiler/src/frontend/npm-static-umd.test.ts index 5280ecb179..14e0d5ea94 100644 --- a/packages/compiler/src/frontend/npm-static-umd.test.ts +++ b/packages/compiler/src/frontend/npm-static-umd.test.ts @@ -104,6 +104,18 @@ test("other shapes keep the original text", () => { expect(unwrap(umd(` exports.a = 1;`, "req, exp"))).toBeNull(); // Code after the wrapper. expect(unwrap(umd(` exports.a = 1;`) + "exports.b = 2;\n")).toBeNull(); + // Other statements in the wrapper, which unwrapping would drop. + const wrapped = umd(` exports.a = 1;`); + const call = " var v = factory(require, exports);\n"; + expect(unwrap(wrapped.replace(call, call + " exports.b = 2;\n"))).toBeNull(); + expect(unwrap(wrapped.replace(call, " exports.b = 2;\n" + call))).toBeNull(); + expect(unwrap(wrapped.replace(" }\n})(", " }\n exports.b = 2;\n})("))).toBeNull(); + expect( + unwrap(wrapped.replace("define.amd) {", "define.amd) {\n exports.b = 2;")), + ).toBeNull(); + expect( + unwrap(wrapped.replace("define(", "exports.b = 2;\n }\n else {\n define(")), + ).toBeNull(); // Not a UMD wrapper. expect(unwrap(`exports.a = 1;\n`)).toBeNull(); }); diff --git a/packages/compiler/src/frontend/npm-static-umd.ts b/packages/compiler/src/frontend/npm-static-umd.ts index 88400e0464..4a5db50659 100644 --- a/packages/compiler/src/frontend/npm-static-umd.ts +++ b/packages/compiler/src/frontend/npm-static-umd.ts @@ -23,8 +23,8 @@ * the module's top level, where its requires and export assignments are the * module's own. * - * Accepted: the wrapper is the file's only statement, its CommonJS branch - * calls the factory as `factory(require, exports)`, the factory's + * Accepted: the wrapper is the file's only statement, the wrapper's body is + * exactly the emit above (so dropping it drops nothing else), the factory's * parameters are exactly `require` and `exports`, and the body uses no * `return` or `arguments` of its own. `this` in the body is the factory's * receiver, not the module's: it may appear only in TypeScript's helper @@ -54,7 +54,7 @@ export function unwrapUmdFactory(sf: ts.SourceFile): string | null { const wrapperParams = wrapper.parameters.nodes; if (wrapperParams.length !== 1 || !isPlainParameter(wrapperParams[0]!)) return null; const factoryName = (wrapperParams[0]!.name as ts.Identifier).text; - if (!callsFactoryForCommonJs(wrapper, factoryName)) return null; + if (!isTypeScriptUmdWrapper(wrapper, factoryName)) return null; const params = factory.parameters.nodes; if ( params.length !== 2 || @@ -83,35 +83,49 @@ function isPlainParameter(p: ts.ParameterDeclaration): boolean { return ts.isIdentifier(p.name) && p.initializer === undefined && p.dotDotDotToken === undefined; } -/** True when the wrapper's first statement is TypeScript's CommonJS test, - * `if (typeof module === "object" && typeof module.exports === "object")`, - * whose branch starts by calling the factory as `factory(require, exports)`. */ -function callsFactoryForCommonJs(wrapper: ts.FunctionExpression, factoryName: string): boolean { - const first = wrapper.body?.statements.nodes[0]; - if (first === undefined || !ts.isIfStatement(first)) return false; +/** True when the wrapper body is exactly TypeScript's emit and nothing else: + * one `if` statement whose CommonJS branch holds only the factory call and + * the `module.exports` assignment, and whose `else`, when present, is only + * the AMD branch: + * + * if (typeof module === "object" && typeof module.exports === "object") { + * var v = factory(require, exports); + * if (v !== undefined) module.exports = v; + * } + * else if (typeof define === "function" && define.amd) { + * define([...], factory); + * } + * + * Unwrapping drops the wrapper, so any other statement in it would be lost. */ +function isTypeScriptUmdWrapper(wrapper: ts.FunctionExpression, factoryName: string): boolean { + const stmts = wrapper.body?.statements.nodes ?? []; + if (stmts.length !== 1 || !ts.isIfStatement(stmts[0]!)) return false; + const test = stmts[0]; const sf = ts.getSourceFileOfNode(wrapper); - const condition = sf.text.slice(tokenStartOfNode(first.expression), first.expression.end); - if ( - condition.replace(/\s+/g, " ") !== - 'typeof module === "object" && typeof module.exports === "object"' - ) + const text = (n: ts.Node): string => + sf.text.slice(tokenStartOfNode(n), n.end).replace(/\s+/g, " "); + if (text(test.expression) !== 'typeof module === "object" && typeof module.exports === "object"') return false; - const branch = first.thenStatement; - const call = ts.isBlock(branch) ? branch.statements.nodes[0] : undefined; - if (call === undefined || !ts.isVariableStatement(call)) return false; + const branch = test.thenStatement; + if (!ts.isBlock(branch) || branch.statements.nodes.length !== 2) return false; + const [call, assign] = branch.statements.nodes as [ts.Statement, ts.Statement]; + if (!ts.isVariableStatement(call)) return false; const declarations = call.declarationList.declarations.nodes; - const init = declarations.length === 1 ? declarations[0]!.initializer : undefined; - if (init === undefined || !ts.isCallExpression(init)) return false; - const args = init.arguments.nodes; - return ( - ts.isIdentifier(init.expression) && - init.expression.text === factoryName && - args.length === 2 && - ts.isIdentifier(args[0]!) && - args[0].text === "require" && - ts.isIdentifier(args[1]!) && - args[1].text === "exports" - ); + const name = declarations.length === 1 ? declarations[0]!.name : undefined; + if (name === undefined || !ts.isIdentifier(name)) return false; + const result = name.text; + if (text(call) !== `var ${result} = ${factoryName}(require, exports);`) return false; + if (text(assign) !== `if (${result} !== undefined) module.exports = ${result};`) return false; + const amd = test.elseStatement; + if (amd === undefined) return true; + if (!ts.isIfStatement(amd) || amd.elseStatement !== undefined) return false; + if (text(amd.expression) !== 'typeof define === "function" && define.amd') return false; + const define = ts.isBlock(amd.thenStatement) ? amd.thenStatement.statements.nodes : []; + if (define.length !== 1 || !ts.isExpressionStatement(define[0]!)) return false; + const defineCall = define[0].expression; + if (!ts.isCallExpression(defineCall) || text(defineCall.expression) !== "define") return false; + const last = defineCall.arguments.nodes.at(-1); + return last !== undefined && ts.isIdentifier(last) && last.text === factoryName; } /** True when the factory body behaves the same as the module's top level: From c29ce641b2d2aba99ae56129450956e128454c08 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 19:26:00 -0500 Subject: [PATCH 021/395] Count only the repeated parts of loops as likely-hot work A `for` loop's initializer and a `for...of` loop's iterable run once, but SC6101 counted any dynamic operation inside a loop statement's span, so untyped setup there produced the warning and failed --fail-on=dynamic. Loop work is now found by walking the IR: a loop's condition, update and body repeat, its initializer and iterable do not. Locations could not tell them apart, because lowering gives the index loop of a `for...of` the whole statement's span. --- docs/content/docs/coverage.mdx | 2 +- .../src/frontend/lowering/dynamic-tier.ts | 103 +++++++++++++----- tests/coverage-fixtures/dynamic-tier/quiet.ts | 16 ++- 3 files changed, 90 insertions(+), 31 deletions(-) diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index 09f87bacdc..d2fc345ad9 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -77,7 +77,7 @@ warning SC6101: 3 functions that likely run often use untyped values, which runs The program compiled. Adding types to these functions lets scriptc compile them to faster code. ``` -Without a profile, a dynamic-tier function counts as likely to run often when a dynamic operation runs inside one of its loops, when a loop calls it, or when it calls itself. A function is named only when its source says why its values are untyped: a value typed `any` (for a `for...of` variable, the value it iterates), an untyped `JSON.parse` result, or an untyped JavaScript file. Dynamic code that runs once, such as setup code that reads an `any` configuration, is not reported, and neither are functions that are dynamic for an operation with no static lowering, which more types would not change. +Without a profile, a dynamic-tier function counts as likely to run often when a dynamic operation runs on every iteration of one of its loops (a `for` loop's initializer and a `for...of` loop's iterable run once), when a loop calls it, or when it calls itself. A function is named only when its source says why its values are untyped: a value typed `any` (for a `for...of` variable, the value it iterates), an untyped `JSON.parse` result, or an untyped JavaScript file. Dynamic code that runs once, such as setup code that reads an `any` configuration, is not reported, and neither are functions that are dynamic for an operation with no static lowering, which more types would not change. The warning names at most five functions, most likely to run often first, and gives the total. Dynamic-tier code in npm packages is summarized on one line with each package's function count. A build that comes from the cache repeats the warning. `--no-dynamic-tier-warning` leaves it out, and `--fail-on=dynamic` fails a coverage run on it. diff --git a/packages/compiler/src/frontend/lowering/dynamic-tier.ts b/packages/compiler/src/frontend/lowering/dynamic-tier.ts index 873b223432..72cba99429 100644 --- a/packages/compiler/src/frontend/lowering/dynamic-tier.ts +++ b/packages/compiler/src/frontend/lowering/dynamic-tier.ts @@ -1,8 +1,8 @@ /* Hot dynamic-tier functions (SC6101, diagnostics/dynamic-tier.ts): the * functions of a program that compile to the dynamic tier (ir/tiers.ts) and * likely run often, so a build can name them. Without a profile, a function - * counts as likely hot when a dynamic operation runs inside one of its - * loops, when a loop calls it, or when it calls itself. Dynamic code that + * counts as likely hot when a dynamic operation runs on every iteration of + * one of its loops, when a loop calls it, or when it calls itself. Dynamic code that * runs once (setup code that reads an `any` configuration, for example) * stays quiet. * @@ -21,8 +21,12 @@ import { type UntypedCause, } from "../../diagnostics/dynamic-tier.js"; import type { IrExpr, IrFunction, IrModule, IrStmt, SrcLoc } from "../../ir/ir.js"; -import { dynamicOperationSites } from "../../ir/tiers.js"; -import { everyStmtList } from "../../ir/traverse.js"; +import { + dynamicOperationOf, + dynamicOperationSites, + type DynamicOperationSite, +} from "../../ir/tiers.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import * as ts from "../ts7/adapter.js"; import { isCheckedJsSourceFile, isJsSourceFile } from "../program.js"; import { isNodeModulesPath } from "../resolve.js"; @@ -46,30 +50,73 @@ export interface DynamicTierReport { packages: DynamicTierPackageCount[]; } -const LOOP_KINDS = new Set(["while", "doWhile", "for", "forOf"]); - -function within(loc: SrcLoc, spans: readonly SrcLoc[]): boolean { - return spans.some((s) => s.file === loc.file && s.start <= loc.start && loc.end <= s.end); -} - interface FunctionFacts { - loops: SrcLoc[]; - /** Callee names of direct calls, with each call's location. */ - calls: { callee: string; loc: SrcLoc }[]; + /** Dynamic operations in the parts of the function's loops that run on + * every iteration. */ + loopSites: DynamicOperationSite[]; + /** Callee names of direct calls, and whether each runs on every + * iteration of a loop. */ + calls: { callee: string; inLoop: boolean }[]; } +/** Walks a function body by structure: a loop's condition, update and body + * repeat, while a `for` loop's initializer and a `for...of` loop's iterable + * run once. (Locations cannot tell them apart: lowering may give a loop's + * repeated parts the whole statement's span.) */ function factsOf(fn: IrFunction): FunctionFacts { - const facts: FunctionFacts = { loops: [], calls: [] }; - everyStmtList(fn.body, { - stmt: (s: IrStmt) => { - if (LOOP_KINDS.has(s.kind)) facts.loops.push(s.loc); - return true; - }, - expr: (e: IrExpr) => { - if (e.kind === "call") facts.calls.push({ callee: e.callee, loc: e.loc }); - return true; - }, - }); + const facts: FunctionFacts = { loopSites: [], calls: [] }; + const expr = (e: IrExpr, inLoop: boolean): void => { + if (inLoop) { + const operation = dynamicOperationOf(e); + if (operation !== null) facts.loopSites.push({ operation, loc: e.loc }); + } + if (e.kind === "call") facts.calls.push({ callee: e.callee, inLoop }); + everyExprChild( + e, + (child) => { + expr(child, inLoop); + return true; + }, + (child) => { + stmt(child, inLoop); + return true; + }, + ); + }; + const children = (s: IrStmt, inLoop: boolean): void => { + everyStmtChild( + s, + (child) => { + expr(child, inLoop); + return true; + }, + (child) => { + stmt(child, inLoop); + return true; + }, + ); + }; + const stmt = (s: IrStmt, inLoop: boolean): void => { + switch (s.kind) { + case "for": + if (s.init !== null) stmt(s.init, inLoop); + if (s.cond !== null) expr(s.cond, true); + if (s.update !== null) stmt(s.update, true); + for (const b of s.body) stmt(b, true); + return; + case "forOf": + expr(s.iterable, inLoop); + for (const b of s.body) stmt(b, true); + return; + case "while": + case "doWhile": + children(s, true); + return; + default: + children(s, inLoop); + } + }; + for (const s of fn.body) stmt(s, false); return facts; } @@ -202,7 +249,7 @@ export function scanDynamicTier(lowerer: Lowerer, module: IrModule): DynamicTier const calledInLoop = new Set(); for (const fn of module.functions) { const f = facts.get(fn.name)!; - for (const call of f.calls) if (within(call.loc, f.loops)) calledInLoop.add(call.callee); + for (const call of f.calls) if (call.inLoop) calledInLoop.add(call.callee); } const hot: HotDynamicFunction[] = []; const packages = new Map(); @@ -218,7 +265,7 @@ export function scanDynamicTier(lowerer: Lowerer, module: IrModule): DynamicTier } const f = facts.get(fn.name)!; const sites = dynamicOperationSites(fn); - const inLoops = sites.filter((s) => within(s.loc, f.loops)); + const inLoops = f.loopSites; const recursive = f.calls.some((c) => c.callee === fn.name); const reason: HotReason | null = inLoops.length > 0 @@ -231,7 +278,9 @@ export function scanDynamicTier(lowerer: Lowerer, module: IrModule): DynamicTier if (reason === null) continue; // Only functions the source makes dynamic are named: ones dynamic for // operations without a static lowering would stay dynamic with more types. - const cause = causeOf(lowerer, [...inLoops, ...sites.filter((s) => !inLoops.includes(s))]); + const looped = new Set(inLoops.map((s) => `${s.loc.file}:${s.loc.start}:${s.loc.end}`)); + const rest = sites.filter((s) => !looped.has(`${s.loc.file}:${s.loc.start}:${s.loc.end}`)); + const cause = causeOf(lowerer, [...inLoops, ...rest]); if (cause === null) continue; hot.push({ name: fn.name, diff --git a/tests/coverage-fixtures/dynamic-tier/quiet.ts b/tests/coverage-fixtures/dynamic-tier/quiet.ts index d045a9392d..1b0a212eab 100644 --- a/tests/coverage-fixtures/dynamic-tier/quiet.ts +++ b/tests/coverage-fixtures/dynamic-tier/quiet.ts @@ -1,5 +1,6 @@ -// An untyped configuration read once at startup: dynamic, but not hot, so -// the build names nothing. The hot loop is typed. +// Untyped values read once (a configuration at startup, a loop's +// initializer, a for...of iterable): dynamic, but not hot, so the build +// names nothing. The loops repeat only typed work. const config: any = JSON.parse('{"scale": 3, "label": "sum"}'); const scale: number = config.scale; const label: string = config.label; @@ -10,4 +11,13 @@ function sum(values: number[]): number { return total; } -console.log(label, sum([1, 2, 3])); +// Untyped values read once, in a loop's initializer and in a for...of +// iterable: the loops repeat only typed work. +function offsets(raw: any): number { + let total = 0; + for (let i: number = raw.start; i < 3; i++) total += i; + for (const n of raw.extra as number[]) total += n; + return total; +} + +console.log(label, sum([1, 2, 3]), offsets({ start: 1, extra: [4, 5] })); From 5bcd68f1aec0783a468d2053e146a86b70f52eb8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 19:31:44 -0500 Subject: [PATCH 022/395] Address the third review round on array named properties --- packages/runtime/src/scr_json.c | 19 ++++++++++++++----- .../6987-array-view-named-properties.ts | 10 ++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index f62ef01655..89007a6acb 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -10279,20 +10279,29 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) { !scr_typed_ref_is_length(key) && !scr_typed_ref_canonical_index(key->data, key->len, &element)) { double number; bool numeric = scr_arr_numeric_key(key->data, key->len, &number); - if (numeric) scr_arr_delete(recv->v.typed_ref.ptr, number); if (recv->v.typed_ref.commit) { ScrDyn *view = scr_dyn_typed_ref_materialize(recv); if (!scr_exc_pending() && view->kind == SCR_DYN_ARR) { - bool own = view->v.arr.properties && scr_dyn_obj_get(view->v.arr.properties, key->data, key->len); - if (own) scr_dyn_key_delete(view, key, strict); - if (own && !scr_exc_pending()) scr_dyn_typed_ref_commit(recv); + ScrDynEntry *own = view->v.arr.properties ? scr_dyn_entry(view->v.arr.properties, key) : NULL; + /* A non-configurable property refuses the delete before either + * store changes. */ + if (own && !own->configurable) { + scr_dyn_key_delete(view, key, strict); + } else { + if (numeric) scr_arr_delete(recv->v.typed_ref.ptr, number); + if (own) scr_dyn_key_delete(view, key, strict); + if (own && !scr_exc_pending()) scr_dyn_typed_ref_commit(recv); + } scr_dyn_release(view); return; } scr_dyn_release(view); if (scr_exc_pending()) return; } - if (numeric) return; + if (numeric) { + scr_arr_delete(recv->v.typed_ref.ptr, number); + return; + } } if (recv->kind == SCR_DYN_FUNC) { if (scr_dyn_fn_property_fence(key)) return; diff --git a/tests/corpus/6987-array-view-named-properties.ts b/tests/corpus/6987-array-view-named-properties.ts index faba7dc90e..ee357d0729 100644 --- a/tests/corpus/6987-array-view-named-properties.ts +++ b/tests/corpus/6987-array-view-named-properties.ts @@ -102,3 +102,13 @@ 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)); From 62ddc2fd352c6f0dadb9cb80b531201cbc7ec3b2 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 19:31:56 -0500 Subject: [PATCH 023/395] Read a class branch's own-property bag from its receiver --- .../lowering/class-dynamic-dispatch.test.ts | 82 +++++++++ .../lowering/class-dynamic-dispatch.ts | 169 +++++++++++++++--- .../corpus/6943-js-class-branch-bags.js.json | 6 + tests/corpus/6943-js-class-branch-bags.js | 67 +++++++ .../6943-js-class-branch-bags.js.json | 1 + 5 files changed, 296 insertions(+), 29 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6943-js-class-branch-bags.js.json create mode 100644 tests/corpus/6943-js-class-branch-bags.js create mode 100644 tests/harness/corpus-costs/6943-js-class-branch-bags.js.json diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts index dfb8bd7a51..6923841bd2 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts @@ -475,3 +475,85 @@ test("normalizes base capsules before dispatching each derived member once", () ], }); }); + +test("class branches read their exact class's bag, also for classes with subclasses", () => { + const { lowerer, liftedFns } = context(); + const base = { + def: { name: "Base", fields: [{ name: "value", type: F64 }], tracksOwnFields: true }, + fields: new Map([["value", F64]]), + methods: new Map(), + base: null, + subclasses: [], + } as unknown as ClassInfo; + const child = { + def: { + name: "Child", + base: "Base", + fields: [{ name: "value", type: F64 }], + tracksOwnFields: true, + }, + fields: new Map([["value", F64]]), + methods: new Map(), + base, + subclasses: [], + } as unknown as ClassInfo; + base.subclasses.push(child); + lowerer.classes.set("Base", base); + lowerer.classes.set("Child", child); + lowerer.classCanBeConstructed = () => true; + lowerer.isSubclassOf = (name, parent) => name === "Child" && parent === "Base"; + lowerer.coerceToExpected = (value, type) => + type.kind === "dyn" ? { kind: "dynFrom", value, type, loc } : value; + const boxes = fn( + "boxes", + [base, child].map((info) => + statement({ + kind: "dynFrom", + value: { + kind: "varRef", + localId: info.def.name, + type: { kind: "object", className: info.def.name }, + loc, + }, + type: DYN, + loc, + }), + ), + ); + const read = fn("read", [ + statement({ + kind: "dynKeyGet", + value: variable("value"), + key: literal("value"), + type: DYN, + loc, + }), + ]); + const dispatch = new ClassDynamicDispatch(); + for (let pass = 0; pass < 4 && dispatch.process(lowerer, [boxes, read, ...liftedFns]); pass++); + dispatch.finalize(lowerer); + const property = liftedFns.find((helper) => helper.name.startsWith("%dyn.class.property."))!; + // The class branches; the fallback for other receivers keeps the + // dispatch over every class. + const branches = property.body.filter( + (statement) => + statement.kind === "if" && + statement.cond.kind === "libCall" && + statement.cond.fn === "dyn.typedRefIs", + ); + expect(branches).toHaveLength(2); + const text = JSON.stringify(branches); + expect(text).toContain('"callee":"%class.branchPropertiesOrEmpty:Base"'); + expect(text).toContain('"callee":"%class.branchPropertiesOrEmpty:Child"'); + expect(text).not.toContain('"callee":"%dyn.class.readPropertiesOrEmpty"'); + const bag = liftedFns.find((helper) => helper.name === "%class.branchPropertiesOrEmpty:Base")!; + // The bag is the receiver's own field; an absent bag reads as a fresh, + // unstored empty object. + expect(bag.params[0]!.type).toEqual({ kind: "object", className: "Base" }); + expect(JSON.stringify(bag.body)).toContain('"className":"Base","field":"%dynProperties"'); + expect(JSON.stringify(bag.body)).not.toContain('"kind":"fieldSet"'); + expect(bag.body[0]).toMatchObject({ + kind: "if", + then: [{ kind: "return", value: { kind: "dynObjLit", fields: [] } }], + }); +}); diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index c891c14cc3..9dd7681a64 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -386,6 +386,26 @@ function readOnlyBagBody(body: IrStmt[], orEmpty = false): IrStmt[] { return body; } +/** `value` with the class branch's checked receiver (`p.0` as a dynamic + * value, possibly cast to the branch's class) replaced by `receiver`, the + * typed receiver of a per-class helper. */ +function withTypedReceiver(value: IrExpr, receiver: IrExpr): IrExpr { + const checked = (e: IrExpr): boolean => + e.kind === "dynCheck" && e.value.kind === "varRef" && e.value.localId === "p.0"; + return transformExpr(value, { + stmt: (stmt) => stmt, + expr: (expr) => + (expr.kind === "downcast" && + checked(expr.value) && + expr.type.kind === "object" && + receiver.type.kind === "object" && + expr.type.className === receiver.type.className) || + checked(expr) + ? receiver + : expr, + }); +} + export function classPropertiesHelper(lowerer: Lowerer, loc: SrcLoc, readOnly = false): IrFunction { const name = readOnly ? READ_PROPERTIES : "%dyn.class.properties"; const existing = lowerer.liftedFnNamed(name); @@ -728,7 +748,11 @@ export class ClassDynamicDispatch { private readonly readPropertyBags: { helper: IrFunction; initialize: Extract; + orEmpty?: boolean; }[] = []; + /** Read-only bags of exactly one class (exactPropertyBag), by class and + * whether an absent bag reads as a fresh empty object. */ + private readonly exactPropertyBags = new Map(); private readonly generated = new Set(); /** Protocol dispatch helpers and callee reads whose receivers normalize. */ private readonly protocols: IrFunction[] = []; @@ -2557,7 +2581,6 @@ export class ClassDynamicDispatch { if (dispatch.classes.has(plan.key) || !matching!.has(info)) continue; dispatch.classes.add(plan.key); const loc = dispatch.source.loc; - const value = varRef("p.0", DYN, loc); const { receiver, condition } = this.propertyReceiver(plan, loc); if (isClassCallback(lowerer, info, dispatch.source.method)) { const prototypeMethod = observedPrototypeMethod( @@ -2567,7 +2590,7 @@ export class ClassDynamicDispatch { loc, receiver, ); - const bag = this.readPropertyBagOrEmpty(lowerer, value, loc); + const bag = this.branchPropertyBag(lowerer, info, receiver, true, loc); const methodKey: IrExpr = { kind: "strLit", value: dispatch.source.method, @@ -2825,10 +2848,19 @@ export class ClassDynamicDispatch { dispatch.classes.add(plan.key); const loc = dispatch.fn.loc; const { receiver, condition } = this.propertyReceiver(plan, loc); + // The branch checks its receiver once: each use of the checked + // receiver would repeat the check. + const local = `receiver.${dispatch.fn.locals.length}`; + dispatch.fn.locals.push({ + id: local, + name: "receiver", + type: receiver.type, + mutable: false, + }); const before = lowerer.diags.length; let branch: IrStmt[]; try { - branch = this.propertyBody(lowerer, dispatch, info, receiver); + branch = this.propertyBody(lowerer, dispatch, info, varRef(local, receiver.type, loc)); } catch (error) { if (!(error instanceof PoisonError) || !info.decl) throw error; const fence = lowerer.deferToRuntimeFence(before, info.decl, { kind: "statement" }); @@ -2838,7 +2870,7 @@ export class ClassDynamicDispatch { dispatch.fn.body.unshift({ kind: "if", cond: condition, - then: branch, + then: [{ kind: "varDecl", localId: local, init: receiver, loc }, ...branch], else_: null, loc, }); @@ -2938,11 +2970,7 @@ export class ClassDynamicDispatch { !info.fields.has(name) && observedPrototypeMethod(lowerer, info, name, loc, receiver); if (info.fields.has(name) || (dispatch.probe === "in" && !prototypeMethod)) { if (info.fields.has(name) && info.def.tracksOwnFields) { - const bag = this.readPropertyBagOrEmpty( - lowerer, - lowerer.coerceToExpected(receiver, DYN), - loc, - ); + const bag = this.branchPropertyBag(lowerer, info, receiver, true, loc); const symbol = symbolMemberKey(lowerer, info, name, loc); const fn = dispatch.probe === "in" @@ -2968,11 +2996,7 @@ export class ClassDynamicDispatch { { kind: "return", value: { kind: "boolLit", value: true, type: BOOL, loc }, loc }, ]; } - const bag = this.readPropertyBagOrEmpty( - lowerer, - lowerer.coerceToExpected(receiver, DYN), - loc, - ); + const bag = this.branchPropertyBag(lowerer, info, receiver, true, loc); const symbol = name.startsWith("sym:") || name.startsWith("%symbol:"); const fn = dispatch.probe === "in" @@ -3042,9 +3066,8 @@ export class ClassDynamicDispatch { } const branches: IrStmt[] = []; if (!dispatch.write && !dispatch.reflect && !info.def.tracksOwnFields) { - const value = varRef("p.0", DYN, loc); const key = varRef("p.key", DYN, loc); - const bag = this.readPropertyBagOrEmpty(lowerer, value, loc); + const bag = this.branchPropertyBag(lowerer, info, receiver, true, loc); branches.push({ kind: "if", cond: { @@ -4070,6 +4093,98 @@ export class ClassDynamicDispatch { return helper; } + /** The read-only bag a class branch reads for its receiver: the + * receiver's own field when the branch selected the receiver's exact + * class, otherwise the dispatch over every class (readPropertyBag, or + * readPropertyBagOrEmpty with `orEmpty`). */ + private branchPropertyBag( + lowerer: Lowerer, + info: ClassInfo, + receiver: IrExpr, + orEmpty: boolean, + loc: SrcLoc, + ): IrExpr { + const helper = this.exactPropertyBag(lowerer, info, receiver, orEmpty, loc); + if (helper) return { kind: "call", callee: helper.name, args: [receiver], type: DYN, loc }; + return orEmpty + ? this.readPropertyBagOrEmpty(lowerer, lowerer.coerceToExpected(receiver, DYN), loc) + : this.readPropertyBag(lowerer, receiver, loc); + } + + /** `%class.branchProperties[OrEmpty]:(receiver)`: the read-only bag of + * an instance of exactly `info`. A class branch of the dispatch helpers + * selects a canonical receiver (normalizeDispatches gives an instance its + * actual class's capsule), so its branch for `info` sees only instances + * of `info`; instances of its subclasses take their own branches. Such a + * receiver needs neither the dispatch over every class nor a boxed copy of + * itself. Null + * when `receiver` is not typed as `info`, when a runtime or builtin class + * in the family keeps its own property contract, or before the class has + * a bag. */ + private exactPropertyBag( + lowerer: Lowerer, + info: ClassInfo, + receiver: IrExpr, + orEmpty: boolean, + loc: SrcLoc, + ): IrFunction | null { + const type = receiver.type; + if (type.kind !== "object" || type.className !== info.def.name) return null; + for (let current: ClassInfo | null = info; current; current = current.base) + if ( + current.def.runtime || + current.builtinEmitter || + current.builtinStream || + current.builtinError + ) + return null; + const key = `${type.className}:${orEmpty}`; + const existing = this.exactPropertyBags.get(key); + if (existing) return existing; + const initializers = this.bagInitializers.get(type.className); + if (!initializers) return null; + const self = varRef("p.0", type, loc); + const bag: IrExpr = { + kind: "fieldGet", + obj: self, + className: type.className, + field: PROPERTY_BAG, + type: DYN, + loc, + }; + const initialize: Extract = { + ...initializers[0]!, + obj: self, + value: withTypedReceiver(initializers[0]!.value, self), + loc, + }; + const helper: IrFunction = { + name: `%class.branchProperties${orEmpty ? "OrEmpty" : ""}:${type.className}`, + params: [{ localId: "p.0", name: "value", type }], + returnType: DYN, + locals: [{ id: "p.0", name: "value", type, mutable: false }], + loc, + body: [ + { + kind: "if", + cond: { kind: "dynTest", test: "undefined", value: bag, type: BOOL, loc }, + then: [initialize], + else_: null, + loc, + }, + { kind: "return", value: bag, loc }, + ], + }; + // Initializers gain their prototype as subclasses become constructible; + // finalize then turns the lazy store into the read's result. + initializers.push(initialize); + this.exactPropertyBags.set(key, helper); + this.readPropertyBags.push({ helper, initialize, orEmpty }); + this.generated.add(helper); + lowerer.liftedFns.push(helper); + return helper; + } + /** The prototype a new property bag of `current` (or of the subclass the * receiver is) starts from, or null when `current` has no prototype data. * A class with constructible subclasses that have prototype data selects @@ -4149,9 +4264,9 @@ export class ClassDynamicDispatch { * prototype-backed bag is produced for this read only. A published object * cannot take the store, and Node's reads never write. */ finalize(lowerer?: Lowerer): void { - for (const { helper, initialize } of this.readPropertyBags) { + for (const { helper, initialize, orEmpty } of this.readPropertyBags) { const absent = helper.body[0]; - if (isEmptyBag(initialize.value)) helper.body.shift(); + if (isEmptyBag(initialize.value) && !orEmpty) helper.body.shift(); else if (absent?.kind === "if") absent.then = [{ kind: "return", value: initialize.value, loc: initialize.loc }]; } @@ -4733,7 +4848,7 @@ export class ClassDynamicDispatch { cond: this.readBagHas( lowerer, "dyn.hasKey", - this.readPropertyBag(lowerer, receiver, loc), + this.branchPropertyBag(lowerer, info, receiver, false, loc), key, loc, ), @@ -4748,7 +4863,7 @@ export class ClassDynamicDispatch { { kind: "return", value: null, loc }, ]; } - const bag = this.readPropertyBag(lowerer, receiver, loc); + const bag = this.branchPropertyBag(lowerer, info, receiver, false, loc); return [ { kind: "return", @@ -4816,7 +4931,7 @@ export class ClassDynamicDispatch { ) { this.constructors.set(info.def.name, info); const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl), DYN); - const bag = this.readPropertyBag(lowerer, receiver, loc); + const bag = this.branchPropertyBag(lowerer, info, receiver, false, loc); const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc }; return [ { @@ -4894,7 +5009,7 @@ export class ClassDynamicDispatch { if (symbol || (isClassOwnEnumerableFieldName(name) && !name.startsWith("sym:"))) { const prototypeMethod = observedPrototypeMethod(lowerer, info, name, loc, receiver); // Selecting the method must not give the object own storage. - const bag = this.readPropertyBag(lowerer, receiver, loc); + const bag = this.branchPropertyBag(lowerer, info, receiver, false, loc); const key: IrExpr = symbol ?? { kind: "strLit", value: name, type: STRING, loc }; body.push({ kind: "if", @@ -4962,7 +5077,7 @@ export class ClassDynamicDispatch { } if (!field && !accessor) { const instance = lowerer.coerceToExpected(receiver, DYN); - const bag = this.readPropertyBagOrEmpty(lowerer, instance, loc); + const bag = this.branchPropertyBag(lowerer, info, receiver, true, loc); const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc }; const own: IrExpr = { kind: "libCall", @@ -5009,11 +5124,7 @@ export class ClassDynamicDispatch { (t.kind === "union" && (getUnion(t.unionId)?.arms.some(mutable) ?? false)); if (boxed.kind === "dynFrom" && mutable(boxed.value.type)) boxed.liveRef = true; if (accessor) { - const bag = this.readPropertyBagOrEmpty( - lowerer, - lowerer.coerceToExpected(receiver, DYN), - loc, - ); + const bag = this.branchPropertyBag(lowerer, info, receiver, true, loc); const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc }; return [ { @@ -5040,7 +5151,7 @@ export class ClassDynamicDispatch { } if (field && info.def.tracksOwnFields) { const instance = lowerer.coerceToExpected(receiver, DYN); - const bag = this.readPropertyBagOrEmpty(lowerer, instance, loc); + const bag = this.branchPropertyBag(lowerer, info, receiver, true, loc); const symbol = symbolMemberKey(lowerer, info, name, loc); const key: IrExpr = symbol ?? { kind: "strLit", value: name, type: STRING, loc }; return [ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6943-js-class-branch-bags.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6943-js-class-branch-bags.js.json new file mode 100644 index 0000000000..31675c66ef --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6943-js-class-branch-bags.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6943-js-class-branch-bags.js" + ], + "diags": [] +} diff --git a/tests/corpus/6943-js-class-branch-bags.js b/tests/corpus/6943-js-class-branch-bags.js new file mode 100644 index 0000000000..b3f91f8d84 --- /dev/null +++ b/tests/corpus/6943-js-class-branch-bags.js @@ -0,0 +1,67 @@ +// Untyped reads of class instances in a hierarchy: own properties added at +// runtime shadow methods and accessors, prototype data answers while an +// instance has no own properties, and computed keys and key probes see the +// same members, for the base class and for each subclass. + +class Shape { + constructor(name) { + this.name = name; + this.sides = 0; + } + area() { + return 0; + } + get label() { + return `${this.name}/${this.sides}`; + } +} +class Square extends Shape { + constructor(size) { + super("square"); + this.sides = 4; + this.size = size; + } + area() { + return this.size * this.size; + } +} +class Tile extends Square { + constructor(size, color) { + super(size); + this.color = color; + } +} +Shape.prototype.kind = "shape"; +Square.prototype.corner = "right"; + +function read(value, key) { + return value[key]; +} +function describe(value) { + const parts = []; + for (const key of ["name", "sides", "size", "color", "kind", "corner", "label", "missing"]) + parts.push(`${key}=${String(read(value, key))}`); + parts.push(`in:${"area" in value},${"kind" in value},${"extra" in value}`); + parts.push(`own:${Object.hasOwn(value, "kind")},${Object.hasOwn(value, "size")}`); + return parts.join(" "); +} + +const shapes = [new Shape("dot"), new Square(3), new Tile(2, "red")]; +for (const shape of shapes) console.log(describe(shape), shape.area(), shape.label); + +const untyped = shapes.map((shape) => /** @type {any} */ (shape)); +untyped[1].area = () => -1; +untyped[2].kind = "tile"; +untyped[2].extra = 7; +try { + untyped[0].label = "plain"; +} catch (error) { + console.log(error.name); +} +Object.defineProperty(untyped[0], "label", { value: "plain", enumerable: true }); +for (const shape of untyped) { + console.log(describe(shape), typeof shape.area, shape.area(), shape.kind, shape.extra); + console.log(read(shape, "area") === Shape.prototype.area, read(shape, "label")); +} +const area = untyped[2].area; +console.log(area.call(new Tile(5, "blue")), Object.keys(untyped[2]).join(",")); diff --git a/tests/harness/corpus-costs/6943-js-class-branch-bags.js.json b/tests/harness/corpus-costs/6943-js-class-branch-bags.js.json new file mode 100644 index 0000000000..9a69600b1c --- /dev/null +++ b/tests/harness/corpus-costs/6943-js-class-branch-bags.js.json @@ -0,0 +1 @@ +{ "plain": 4400, "san": 4900 } From ed5f54c93684b4dbfa87f6e3b6903b4ffd9d1a7b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 19:51:21 -0500 Subject: [PATCH 024/395] Keep constructor types of fetched provenance sources unmapped Constructor types declared in the program hold class values as checked-dynamic values since #1725. A fetched provenance source counts as a program file, so the unused `@__PURE__` const typed `{ new (): any }` in the provenance fixture now had a mapping, no longer elided, and its initializer refused (SC1090). Fetched sources keep their constructor types unmapped, as before #1725. --- packages/compiler/src/frontend/type-mapper.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index b0e3a6dc96..37d6a58072 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -46,6 +46,7 @@ import { literalValues } from "./literal-values.js"; import { remapUnionDiscriminant } from "./union-discriminants.js"; import { isJsSourceFile, isNodeTypesPath } from "./program.js"; +import { isProvenanceSourceFile } from "./provenance-registry.js"; import { functionCanReturnUndefined } from "./function-completion.js"; import { accessorSlotProp, recordTextCodecClass } from "../ir/ir.js"; // typeKey moved to ir/ir.ts (the backend needs it too, for per-type @@ -2466,12 +2467,17 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // value rides the checked-dynamic tree, as an `unknown` slot already // holds one. `new` through it and static reads dispatch at run time, // and the constructed value checks into the signature's instance type. - // Lib and package constructables keep their fences. + // Lib and package constructables keep their fences, and so do those of + // fetched provenance sources (third-party code under its own policies: + // an unused `@__PURE__` const typed `{ new (): any }` elides there). if ( ctorSigs.length > 0 && checker.getCallSignatures(widened).length === 0 && (widened.getSymbol()?.declarations.length ?? 0) > 0 && - widened.getSymbol()!.declarations.every((d) => ctx.isProgramFile(ts.getSourceFileOfNode(d))) + widened.getSymbol()!.declarations.every((d) => { + const sf = ts.getSourceFileOfNode(d); + return ctx.isProgramFile(sf) && !isProvenanceSourceFile(sf.fileName); + }) ) { return DYN; } From c8a524c368ace54c88a4f9720ffad486e61f75f6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 19:59:04 -0500 Subject: [PATCH 025/395] Keep the .d.ts-only types of a package compiled from its JavaScript --- .../frontend/npm-static-type-exports-host.ts | 164 +++++++++++++++ .../frontend/npm-static-type-exports.test.ts | 76 +++++++ .../src/frontend/npm-static-type-exports.ts | 188 ++++++++++++++++++ packages/compiler/src/frontend/npm-static.ts | 28 +++ packages/compiler/src/frontend/program.ts | 6 + .../7001-package-declared-types/main.ts.json | 6 + .../7001-package-declared-types/main.ts | 19 ++ .../node_modules/issue-list/lib/index.d.ts | 17 ++ .../node_modules/issue-list/lib/index.js | 24 +++ .../node_modules/issue-list/lib/paging.d.ts | 4 + .../node_modules/issue-list/lib/paging.js | 2 + .../node_modules/issue-list/package.json | 1 + .../7001-package-declared-types/main.ts.json | 1 + 13 files changed, 536 insertions(+) create mode 100644 packages/compiler/src/frontend/npm-static-type-exports-host.ts create mode 100644 packages/compiler/src/frontend/npm-static-type-exports.test.ts create mode 100644 packages/compiler/src/frontend/npm-static-type-exports.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7001-package-declared-types/main.ts.json create mode 100644 tests/corpus/7001-package-declared-types/main.ts create mode 100644 tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/index.d.ts create mode 100644 tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/index.js create mode 100644 tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/paging.d.ts create mode 100644 tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/paging.js create mode 100644 tests/corpus/7001-package-declared-types/node_modules/issue-list/package.json create mode 100644 tests/harness/corpus-costs/7001-package-declared-types/main.ts.json diff --git a/packages/compiler/src/frontend/npm-static-type-exports-host.ts b/packages/compiler/src/frontend/npm-static-type-exports-host.ts new file mode 100644 index 0000000000..809464763c --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-type-exports-host.ts @@ -0,0 +1,164 @@ +/** Filesystem side of npm-static-type-exports.ts: finds each opted-in + * package's declaration entry and runtime entry, copies the declaration + * files the entry reaches, and builds the runtime entry's typedefs. */ +import { dirname, relative, resolve as resolvePath } from "node:path"; +import { trackedFileExists, trackedReadFile } from "./input-tracker.js"; +import { resolveBareModule } from "./resolve.js"; +import { isJsSourceFileName } from "./tsc-codes.js"; +import type { FrontendServices } from "./services.js"; +import * as ts from "./ts7/syntax.js"; +import { + NPM_STATIC_TYPES_SUFFIX, + npmStaticDeclarationCopy, + npmStaticDeclarationExports, + npmStaticDeclarationSpecifiers, + npmStaticTypedefBlock, + type NpmStaticDeclarationExports, + type NpmStaticTypeDeclaration, +} from "./npm-static-type-exports.js"; + +export interface NpmStaticTypeExportPlan { + /** Copies of the declaration files, by copy path. */ + copies: Map; + /** The typedef block to append, by runtime entry (forward slashes). */ + typedefs: Map; +} + +/** The copy path of a declaration file. */ +function copyPathOf(declarationFile: string): string { + return declarationFile.replace(/\.d\.ts$/, NPM_STATIC_TYPES_SUFFIX); +} + +/** The declaration file a relative specifier names from `fromFile`, or null. */ +function declarationTargetOf(fromFile: string, specifier: string): string | null { + const base = resolvePath(dirname(fromFile), specifier); + const candidates = [ + `${base}.d.ts`, + `${base.replace(/\.(c|m)?js$/, "")}.d.ts`, + resolvePath(base, "index.d.ts"), + ]; + for (const candidate of candidates) if (trackedFileExists(candidate)) return candidate; + return null; +} + +/** A specifier for `target` from `fromFile`'s directory, without `.d.ts`. */ +function specifierFor(fromFile: string, target: string): string { + const rel = relative(dirname(fromFile), target) + .split("\\") + .join("/") + .replace(/\.d\.ts$/, ""); + return rel.startsWith(".") ? rel : `./${rel}`; +} + +/** The names the runtime JavaScript declares as types itself: classes, + * JSDoc typedefs and callbacks, and `@enum` constants. */ +function runtimeTypeNames(services: FrontendServices, file: string, text: string): Set { + const names = new Set(); + for (const m of text.matchAll(/@(?:typedef|callback)\s+(?:\{[^}]*\}\s*)?([A-Za-z_$][\w$]*)/g)) + names.add(m[1]!); + // `/** @enum {number} */ const Kind = { ... }`: the constant names a type too. + for (const m of text.matchAll( + /@enum\b[^\n]*\n(?:\s*\*[^\n]*\n)*?\s*(?:\*\/\s*)?(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)/g, + )) + names.add(m[1]!); + const sf = services.parse(file, text, "js"); + for (const stmt of sf.statements.nodes) + if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) names.add(stmt.name.text); + return names; +} + +export function npmStaticTypeExportPlan( + services: FrontendServices, + entryPath: string, + packages: readonly string[], +): NpmStaticTypeExportPlan { + const copies = new Map(); + const typedefs = new Map(); + const parsed = new Map(); + const parse = (file: string): ts.SourceFile | null => { + let sf = parsed.get(file); + if (sf === undefined) { + const text = trackedReadFile(file); + sf = text === null ? null : services.parse(file, text, "ts"); + parsed.set(file, sf); + } + return sf; + }; + // Copies every declaration file reachable from `file` through relative + // specifiers, each specifier respelled to its target's copy. + const copy = (file: string): void => { + const copyPath = copyPathOf(file); + if (copies.has(copyPath)) return; + const sf = parse(file); + if (sf === null) return; + copies.set(copyPath, ""); + const targets = new Map(); + for (const specifier of npmStaticDeclarationSpecifiers(sf)) { + const target = declarationTargetOf(file, specifier); + if (target !== null) targets.set(specifier, target); + } + copies.set( + copyPath, + npmStaticDeclarationCopy(sf, (specifier) => { + const target = targets.get(specifier); + return target === undefined ? null : specifierFor(copyPath, copyPathOf(target)); + }), + ); + for (const target of targets.values()) copy(target); + }; + // The exported type-only declarations of `file`, through its re-exports. + const typesOf = ( + file: string, + seen: Set, + ): { types: Map; values: Set } => { + const result = { + types: new Map(), + values: new Set(), + }; + if (seen.has(file)) return result; + seen.add(file); + const sf = parse(file); + if (sf === null) return result; + const own: NpmStaticDeclarationExports = npmStaticDeclarationExports(sf); + for (const t of own.types) result.types.set(t.name, t); + for (const v of own.values) result.values.add(v); + for (const n of own.named) { + const target = n.specifier === null ? null : declarationTargetOf(file, n.specifier); + if (target === null) continue; + const inner = typesOf(target, new Set(seen)); + const t = inner.types.get(n.local); + if (t !== undefined) result.types.set(n.exported, { ...t, name: n.exported }); + else if (inner.values.has(n.local)) result.values.add(n.exported); + } + for (const specifier of own.stars) { + const target = declarationTargetOf(file, specifier); + if (target === null) continue; + const inner = typesOf(target, new Set(seen)); + for (const [name, t] of inner.types) if (!result.types.has(name)) result.types.set(name, t); + for (const v of inner.values) result.values.add(v); + } + return result; + }; + for (const pkg of packages) { + const declared = resolveBareModule(entryPath, pkg, "types-only"); + if (declared === null || declared.packageName !== pkg || !declared.typesFile.endsWith(".d.ts")) + continue; + const runtime = resolveBareModule(entryPath, pkg, "js-only"); + if (runtime === null || !isJsSourceFileName(runtime.typesFile)) continue; + const runtimeText = trackedReadFile(runtime.typesFile); + if (runtimeText === null) continue; + const { types, values } = typesOf(declared.typesFile, new Set()); + const declaredByRuntime = runtimeTypeNames(services, runtime.typesFile, runtimeText); + const exported = [...types.values()].filter( + (t) => !values.has(t.name) && !declaredByRuntime.has(t.name), + ); + if (exported.length === 0) continue; + copy(declared.typesFile); + const specifier = specifierFor(runtime.typesFile, copyPathOf(declared.typesFile)); + typedefs.set( + runtime.typesFile.split("\\").join("/"), + npmStaticTypedefBlock(exported, specifier), + ); + } + return { copies, typedefs }; +} diff --git a/packages/compiler/src/frontend/npm-static-type-exports.test.ts b/packages/compiler/src/frontend/npm-static-type-exports.test.ts new file mode 100644 index 0000000000..fa95c0fe08 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-type-exports.test.ts @@ -0,0 +1,76 @@ +import { expect, test } from "vitest"; +import { nodeFrontendServices } from "./services-node.js"; +import { + npmStaticDeclarationCopy, + npmStaticDeclarationExports, + npmStaticTypedefBlock, +} from "./npm-static-type-exports.js"; + +function parse(text: string) { + return nodeFrontendServices().parse("/pkg/lib/index.d.ts", text, "ts"); +} + +test("type-only exports are told from values and re-exports", () => { + const exports = npmStaticDeclarationExports( + parse(`import type { Page } from "./paging"; +export * from "./paging"; +export { Entry as Item, Kind } from "./entries"; +export interface Options { value: T; label: U } +export type Mode = "a" | "b"; +export declare const enum Level { Low = 1 } +export declare class Client { url: string } +export declare function make(options: Options): Client; +export declare const version: string; +interface Hidden { x: number } +export { Hidden as Visible }; +declare function helper(): void; +export { helper };`), + ); + expect(exports.types.map((t) => [t.name, t.params, t.templates])).toEqual([ + ["Options", ["T", "U"], ["{object} [T={}]", "[U=string]"]], + ["Mode", [], []], + ["Level", [], []], + ["Visible", [], []], + ]); + expect(exports.values).toEqual(["Client", "make", "version", "helper"]); + expect(exports.named).toEqual([ + { exported: "Item", local: "Entry", specifier: "./entries" }, + { exported: "Kind", local: "Kind", specifier: "./entries" }, + ]); + expect(exports.stars).toEqual(["./paging"]); +}); + +test("a copy respells the relative specifiers it is told", () => { + const sf = parse(`import type { Page } from "./paging"; +export * from './entries'; +export type Box = import("./paging").Page; +export { readFile } from "fs"; +`); + const copy = npmStaticDeclarationCopy(sf, (s) => + s === "./paging" ? "./paging.scriptc-types" : null, + ); + expect(copy).toBe(`import type { Page } from "./paging.scriptc-types"; +export * from './entries'; +export type Box = import("./paging.scriptc-types").Page; +export { readFile } from "fs"; +`); +}); + +test("the typedef block re-exports each type with its parameters", () => { + expect( + npmStaticTypedefBlock( + [ + { name: "Mode", params: [], templates: [] }, + { name: "Options", params: ["T", "U"], templates: ["{object} [T={}]", "[U=string]"] }, + ], + "./index.scriptc-types", + ), + ).toBe(`/** + * @typedef {import("./index.scriptc-types").Mode} Mode + */ +/** + * @template {object} [T={}] + * @template [U=string] + * @typedef {import("./index.scriptc-types").Options} Options + */`); +}); diff --git a/packages/compiler/src/frontend/npm-static-type-exports.ts b/packages/compiler/src/frontend/npm-static-type-exports.ts new file mode 100644 index 0000000000..844afa5900 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-type-exports.ts @@ -0,0 +1,188 @@ +/* The type-only exports of an opted-in package's declaration files. + * + * An opted-in package compiles from its JavaScript, and the checker types + * the package from it: the package's shipped .d.ts is hidden from + * resolution (npmStaticFsShadow). Values keep their JavaScript types, but a + * type that exists only in the .d.ts (an interface, a type alias, an enum + * used as a type) has no declaration in the JavaScript, so a program's + * `import type { Options } from "pkg"` resolved to `any`. TypeScript and + * Node see the .d.ts types. + * + * The package's declaration files stay available as copies under another + * name (`.scriptc-types.d.ts`, relative specifiers respelled to the + * copies), and the runtime entry gains a JSDoc typedef per type-only export + * that names the declaration in the copy: + * + * / ** @template T @typedef {import("./main.scriptc-types").Box} Box * / + * + * Classes and values keep the JavaScript's own declarations. */ +import * as ts from "./ts7/syntax.js"; +import { tokenStartOfNode } from "./ts7/node-functions.js"; + +/** The suffix that replaces `.d.ts` in a copy's name. */ +export const NPM_STATIC_TYPES_SUFFIX = ".scriptc-types.d.ts"; + +/** A type-only declaration: its name, its type parameters' names and their + * JSDoc @template tag bodies. */ +export interface NpmStaticTypeDeclaration { + name: string; + params: readonly string[]; + templates: readonly string[]; +} + +/** What a declaration file declares and re-exports, by exported name. */ +export interface NpmStaticDeclarationExports { + /** Exported interfaces, type aliases and enums, with their parameters. */ + types: readonly NpmStaticTypeDeclaration[]; + /** Exported classes, functions, variables and namespaces: names the + * JavaScript declares itself, or that are not type-only. */ + values: readonly string[]; + /** `export { local as exported } from "specifier"` and local + * `export { a, b }` (specifier null). */ + named: readonly { exported: string; local: string; specifier: string | null }[]; + /** `export * from "specifier"`. */ + stars: readonly string[]; +} + +function isExported(node: ts.Node): boolean { + const modifiers = (node as { modifiers?: { nodes: readonly ts.Node[] } }).modifiers?.nodes ?? []; + return modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword); +} + +function templatesOf(sf: ts.SourceFile, params: readonly ts.TypeParameterDeclaration[]): string[] { + const text = (n: ts.Node): string => sf.text.slice(tokenStartOfNode(n), n.end); + return params.map((p) => { + const name = p.name.text; + const constraint = p.constraint === undefined ? "" : `{${text(p.constraint)}} `; + return p.defaultType === undefined + ? `${constraint}${name}` + : `${constraint}[${name}=${text(p.defaultType)}]`; + }); +} + +/** The exports of one declaration file. */ +export function npmStaticDeclarationExports(sf: ts.SourceFile): NpmStaticDeclarationExports { + const types: NpmStaticTypeDeclaration[] = []; + const values: string[] = []; + const named: { exported: string; local: string; specifier: string | null }[] = []; + const stars: string[] = []; + const localTypes = new Map(); + const localValues = new Set(); + for (const stmt of sf.statements.nodes) { + const exported = isExported(stmt); + if (ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)) { + const params = stmt.typeParameters?.nodes ?? []; + const decl = { + name: stmt.name.text, + params: params.map((p) => p.name.text), + templates: templatesOf(sf, params), + }; + localTypes.set(decl.name, decl); + if (exported) types.push(decl); + } else if (ts.isEnumDeclaration(stmt)) { + const decl = { name: stmt.name.text, params: [], templates: [] }; + localTypes.set(decl.name, decl); + if (exported) types.push(decl); + } else if ( + ts.isClassDeclaration(stmt) || + ts.isFunctionDeclaration(stmt) || + ts.isModuleDeclaration(stmt) + ) { + const name = stmt.name !== undefined && ts.isIdentifier(stmt.name) ? stmt.name.text : null; + if (name !== null) { + localValues.add(name); + if (exported) values.push(name); + } + } else if (ts.isVariableStatement(stmt)) { + for (const d of stmt.declarationList.declarations.nodes) { + if (!ts.isIdentifier(d.name)) continue; + localValues.add(d.name.text); + if (exported) values.push(d.name.text); + } + } else if (ts.isExportDeclaration(stmt)) { + const specifier = + stmt.moduleSpecifier !== undefined && ts.isStringLiteral(stmt.moduleSpecifier) + ? stmt.moduleSpecifier.text + : null; + const clause = stmt.exportClause; + if (clause === undefined) { + if (specifier !== null) stars.push(specifier); + } else if (ts.isNamedExports(clause)) { + for (const e of clause.elements.nodes) { + const exportedName = e.name.text; + const local = (e.propertyName ?? e.name).text; + named.push({ exported: exportedName, local, specifier }); + } + } + } + } + // Local `export { a as b }` of the file's own declarations. + const resolvedNamed: typeof named = []; + for (const n of named) { + if (n.specifier !== null) { + resolvedNamed.push(n); + continue; + } + const local = localTypes.get(n.local); + if (local !== undefined) types.push({ ...local, name: n.exported }); + else if (localValues.has(n.local)) values.push(n.exported); + } + return { types, values, named: resolvedNamed, stars }; +} + +/** The relative module specifiers of a declaration file's imports, exports + * and `import("...")` types, with their spans. */ +function relativeSpecifiers(sf: ts.SourceFile): { start: number; end: number; text: string }[] { + const found: { start: number; end: number; text: string }[] = []; + const add = (literal: ts.Node | undefined): void => { + if (literal === undefined || !ts.isStringLiteral(literal)) return; + if (!literal.text.startsWith("./") && !literal.text.startsWith("../")) return; + found.push({ start: tokenStartOfNode(literal), end: literal.end, text: literal.text }); + }; + ts.walkPreorder(sf, (n) => { + if (ts.isImportDeclaration(n) || ts.isExportDeclaration(n)) add(n.moduleSpecifier); + else if (ts.isImportTypeNode(n) && ts.isLiteralTypeNode(n.argument)) add(n.argument.literal); + return undefined; + }); + return found; +} + +/** The relative specifiers a declaration file names. */ +export function npmStaticDeclarationSpecifiers(sf: ts.SourceFile): string[] { + return relativeSpecifiers(sf).map((s) => s.text); +} + +/** The declaration file's text with each relative specifier `rename` + * answers respelled (quoted with the original's quote). */ +export function npmStaticDeclarationCopy( + sf: ts.SourceFile, + rename: (specifier: string) => string | null, +): string { + const spans = relativeSpecifiers(sf).sort((a, b) => a.start - b.start); + let text = ""; + let at = 0; + for (const span of spans) { + const renamed = rename(span.text); + if (renamed === null) continue; + const quote = sf.text[span.start] ?? '"'; + text += sf.text.slice(at, span.start) + quote + renamed + quote; + at = span.end; + } + return text + sf.text.slice(at); +} + +/** The JSDoc typedefs that re-export `types` from the copy at `specifier`. */ +export function npmStaticTypedefBlock( + types: readonly NpmStaticTypeDeclaration[], + specifier: string, +): string { + const lines: string[] = []; + for (const t of types) { + const args = t.params.length === 0 ? "" : `<${t.params.join(", ")}>`; + lines.push("/**"); + for (const tag of t.templates) lines.push(` * @template ${tag}`); + lines.push(` * @typedef {import(${JSON.stringify(specifier)}).${t.name}${args}} ${t.name}`); + lines.push(" */"); + } + return lines.join("\n"); +} diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index 56edafc48e..33ff54cfd5 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -16,12 +16,14 @@ import { } from "./workspace-registry.js"; import { clearVirtualSources, + isVirtualSource, setVirtualSource, trackedExists, trackedReadFile, trackedRealpath, } from "./input-tracker.js"; import { npmBuiltinAliasOf } from "./builtin-modules.js"; +import { NPM_STATIC_TYPES_SUFFIX } from "./npm-static-type-exports.js"; let activePackages: ReadonlySet = new Set(); let declarationOverloads: ReadonlyMap = new Map(); @@ -53,10 +55,27 @@ export function npmStaticLexerVisibleNames(path: string): readonly string[] { return lexerNames.get(path.split("\\").join("/")) ?? []; } +/** Per runtime entry (forward slashes), the JSDoc typedefs that re-export + * its package's type-only declarations (npm-static-type-exports.ts). */ +let typeExportTypedefs = new Map(); + +/** Registers the copies of the opted-in packages' declaration files and the + * typedefs their runtime entries gain (after setNpmStaticPackages, which + * clears virtual sources). */ +export function setNpmStaticTypeExports( + copies: ReadonlyMap, + typedefs: ReadonlyMap, +): void { + for (const [path, text] of copies) setVirtualSource(path, text); + typeExportTypedefs = new Map(typedefs); + rewriteCache.clear(); +} + export function setNpmStaticPackages>(packages: T): void { // npm packages served as builtins (graceful-fs as fs) never compile // their JavaScript, even when named. activePackages = new Set([...packages].filter((name) => npmBuiltinAliasOf(name) === null)); + typeExportTypedefs = new Map(); declarationOverloads = new Map(); declarationProperties = new Map(); offenders.clear(); @@ -524,12 +543,21 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow } catch { rewritten = null; // unreadable/unparseable: fall through untouched } + // The runtime entry re-exports the type-only declarations of the + // package's .d.ts, after every respelling, so positions stay. + const typedefs = typeExportTypedefs.get(path.split("\\").join("/")); + if (typedefs !== undefined) { + const text = rewritten ?? trackedReadFile(path); + if (text !== null) rewritten = `${text}\n${typedefs}\n`; + } rewriteCache.set(path, rewritten); return rewritten ?? undefined; } return undefined; }, hideFile: (path) => { + // The copies of the package's declaration files the typedefs name. + if (path.endsWith(NPM_STATIC_TYPES_SUFFIX) && isVirtualSource(path)) return false; const target = shadowTargetOf(path); if (!target) return false; if (isBundledShimForwarder(path)) return false; diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index db43b77492..e398e59df7 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -76,8 +76,10 @@ import { reportNpmStaticOffender, setNpmStaticDeclarationOverloads, setNpmStaticPackages, + setNpmStaticTypeExports, } from "./npm-static.js"; import { isPrunedNpmReexport, planNpmStaticReexports } from "./npm-static-prune.js"; +import { npmStaticTypeExportPlan } from "./npm-static-type-exports-host.js"; import { isNpmStaticSubclassArgument } from "./npm-static-subtyping.js"; import { npmStaticDeclarationReexports, @@ -1032,6 +1034,10 @@ export function loadProgram( } setNpmStaticPackages(npmStaticPackages); setNpmStaticDeclarationOverloads(declarationOverloads, declarationProperties); + // Types only the packages' .d.ts declares keep their declarations + // (npm-static-type-exports.ts). + const typeExports = npmStaticTypeExportPlan(services, entryPath, npmStaticPackages); + setNpmStaticTypeExports(typeExports.copies, typeExports.typedefs); // Workspace-package registrations reset per load (same discipline as the // npm-static set), then the opted-in names are probed UP FRONT: a // workspace-linked opt-in resolves to files whose realpaths carry no diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7001-package-declared-types/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7001-package-declared-types/main.ts.json new file mode 100644 index 0000000000..c5aa8bb422 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7001-package-declared-types/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7001-package-declared-types/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7001-package-declared-types/main.ts b/tests/corpus/7001-package-declared-types/main.ts new file mode 100644 index 0000000000..c5194d33d2 --- /dev/null +++ b/tests/corpus/7001-package-declared-types/main.ts @@ -0,0 +1,19 @@ +// A package that compiles from its JavaScript, whose interfaces, type +// aliases and enums exist only in its .d.ts. A program that imports its +// values compiles the JavaScript, and the type-only names it imports keep +// the declarations TypeScript reads from the .d.ts. +import { Severity, collect, firstOf, label, type Issue, type Page, type Report } from "issue-list"; + +function report(text: string): Report { + const issues: Issue[] = []; + const count = collect(text, issues); + return { count, issues }; +} + +const result = report("a?b!c?"); +console.log(result.count, result.issues.length); +const first = result.issues[0]!; +console.log(label(first.severity), first.offset, first.severity === Severity.Warning); +const page: Page = { items: result.issues.slice(1), next: null }; +console.log(page.items.map((issue) => `${label(issue.severity)}@${issue.offset}`).join(","), page.next); +console.log(firstOf(page)?.offset); diff --git a/tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/index.d.ts b/tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/index.d.ts new file mode 100644 index 0000000000..5ba9d04aca --- /dev/null +++ b/tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/index.d.ts @@ -0,0 +1,17 @@ +import type { Page } from "./paging"; +export * from "./paging"; +export declare enum Severity { + Warning = 1, + Error = 2 +} +export interface Issue { + severity: Severity; + offset: number; +} +export type Report = { + count: number; + issues: Issue[]; +}; +export declare function collect(text: string, issues: Issue[]): number; +export declare function label(severity: Severity): string; +export declare function firstOf(page: Page): T | undefined; diff --git a/tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/index.js b/tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/index.js new file mode 100644 index 0000000000..516d2fa30e --- /dev/null +++ b/tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/index.js @@ -0,0 +1,24 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Severity = void 0; +exports.collect = collect; +exports.label = label; +exports.firstOf = firstOf; +var Severity; +(function (Severity) { + Severity[Severity["Warning"] = 1] = "Warning"; + Severity[Severity["Error"] = 2] = "Error"; +})(Severity || (exports.Severity = Severity = {})); +function collect(text, issues) { + for (let i = 0; i < text.length; i++) { + if (text[i] === "?") issues.push({ severity: Severity.Warning, offset: i }); + else if (text[i] === "!") issues.push({ severity: Severity.Error, offset: i }); + } + return issues.length; +} +function label(severity) { + return severity === Severity.Warning ? "warning" : "error"; +} +function firstOf(page) { + return page.items[0]; +} diff --git a/tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/paging.d.ts b/tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/paging.d.ts new file mode 100644 index 0000000000..c427d5ce0b --- /dev/null +++ b/tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/paging.d.ts @@ -0,0 +1,4 @@ +export interface Page { + items: T[]; + next: string | null; +} diff --git a/tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/paging.js b/tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/paging.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/tests/corpus/7001-package-declared-types/node_modules/issue-list/lib/paging.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/tests/corpus/7001-package-declared-types/node_modules/issue-list/package.json b/tests/corpus/7001-package-declared-types/node_modules/issue-list/package.json new file mode 100644 index 0000000000..dcb8f20c33 --- /dev/null +++ b/tests/corpus/7001-package-declared-types/node_modules/issue-list/package.json @@ -0,0 +1 @@ +{ "name": "issue-list", "version": "1.0.0", "main": "./lib/index.js", "types": "./lib/index.d.ts" } diff --git a/tests/harness/corpus-costs/7001-package-declared-types/main.ts.json b/tests/harness/corpus-costs/7001-package-declared-types/main.ts.json new file mode 100644 index 0000000000..d34ce5c691 --- /dev/null +++ b/tests/harness/corpus-costs/7001-package-declared-types/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2683, "san": 1176 } From 1c03c0c17f5ea2c190d12075662345b440e991cf Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 20:07:00 -0500 Subject: [PATCH 026/395] Skip the pending test after callbacks that cannot throw An array helper specialized for its callback calls the callback's function directly once the closure's entry matches it, and falls back to the entry otherwise. Both paths were followed by the pending-exception test. When the direct target is outside the may-throw set, only the fallback can leave an exception pending, so the test now runs behind a guard that is true on that path alone; a known closure called directly skips it. Worker programs keep the test on every path, where it also observes termination. --- packages/compiler/src/backend/llvm/emitter.ts | 25 +++++++- .../compiler/src/backend/llvm/expr-calls.ts | 17 +++++- .../test/callback-pending-emission.test.ts | 57 +++++++++++++++++++ 3 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/test/callback-pending-emission.test.ts diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 1dcd1f6c85..4504ae58e6 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -2496,8 +2496,14 @@ export class LlEmitter { * releases the dummy (NULL for refcounted kinds) harmlessly. A scalar * call result of the function's return type may be passed as the unwind's * dummy value (see emitUnwind). `poll` marks a worker termination poll - * (function entry or loop budget), whose test must stay ordered. */ - emitPendingCheck(passThrough?: { name: string; type: IrType }, poll = false): void { + * (function entry or loop budget), whose test must stay ordered. A + * `guard` (an i1) limits the test to the paths where it is true: false + * where the joined call could not have left anything pending. */ + emitPendingCheck( + passThrough?: { name: string; type: IrType }, + poll = false, + guard?: string, + ): void { const through = passThrough !== undefined && this.llType(passThrough.type) === this.llType(this.currentReturnType) && @@ -2506,6 +2512,13 @@ export class LlEmitter { : undefined; const B = this.B; if (B.isTerminated()) return; + let skip: string | undefined; + if (guard !== undefined) { + const test = B.newLabel("exc.t"); + skip = B.newLabel("exc.n"); + B.condBr(guard, test, skip); + B.startBlock(test); + } if (this.mod.lib !== undefined) { this.declare(`declare zeroext i1 @scr_exc_pending()`); const p = B.tmp(); @@ -2516,6 +2529,10 @@ export class LlEmitter { B.startBlock(lu); this.emitUnwind(through); B.startBlock(lk); + if (skip !== undefined) { + B.br(skip); + B.startBlock(skip); + } return; } // Executables test the active exception cell's kind inline (its first @@ -2588,6 +2605,10 @@ export class LlEmitter { B.startBlock(lu); this.emitUnwind(through); B.startBlock(lk); + if (skip !== undefined) { + B.br(skip); + B.startBlock(skip); + } } /** After a runtime call that ALWAYS leaves an exception pending (the diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index d02c43c32d..81c44d5a6b 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -921,9 +921,18 @@ export function emitCallExpr( ? `@${mangleBorrowedFunction(known!.name)}` : `@${mangleBorrowingWrapper(known!.name)}` : undefined; + // A known target outside the may-throw set leaves nothing pending, so + // only the entry through the closure's own pointer needs the test. + // Worker programs keep it on every path: there it also observes + // termination, which loops count on (callTestsPending). + const quiet = + known !== undefined && !host.mayThrow.has(known.name) && host.mod.workers !== true; + let pending = host.indirectMayThrow; + let pendingGuard: string | undefined; let t: string; if (direct && e.callee.kind === "closure") { t = invoke(lending ?? direct, !host.receiverReaders.ignores(known!)); + if (quiet) pending = false; } else { const fnp = B.tmp(), fn = B.tmp(); @@ -983,17 +992,21 @@ export function emitCallExpr( B.line( `${t} = phi ${host.llType(e.type)} [ ${specialized}, %${fastEnd} ], [ ${indirect}, %${fallbackEnd} ]`, ); + if (quiet && pending) { + pendingGuard = B.tmp(); + B.line(`${pendingGuard} = phi i1 [ false, %${fastEnd} ], [ true, %${fallbackEnd} ]`); + } } } if (e.type.kind === "void") { - if (host.indirectMayThrow) host.emitPendingCheck(); + if (pending) host.emitPendingCheck(undefined, false, pendingGuard); if (lent) host.releaseTemps(host.frames.pop()!); return { name: "", type: e.type }; } // The check runs AFTER the result temp joins the frame: an unwind // releases it (the dummy is NULL for refcounted returns). const out = host.own({ name: t, type: e.type }); - if (host.indirectMayThrow) host.emitPendingCheck(); + if (pending) host.emitPendingCheck(undefined, false, pendingGuard); if (lent) { host.toFrame(out); host.releaseTemps(host.frames.pop()!); diff --git a/packages/compiler/test/callback-pending-emission.test.ts b/packages/compiler/test/callback-pending-emission.test.ts new file mode 100644 index 0000000000..9dcbbf0a18 --- /dev/null +++ b/packages/compiler/test/callback-pending-emission.test.ts @@ -0,0 +1,57 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule, validateModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; + +// A specialized array helper calls its known callback directly after +// checking the closure's entry. A callback that cannot throw skips the +// pending test on that path, and the fallback through the closure's own +// entry keeps it; a callback that can throw keeps it on both paths. +test("known callbacks outside the may-throw set test the pending flag only on the fallback", async () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-callback-pending-")); + try { + const entry = join(dir, "main.ts"); + const output = join(dir, "main.ir.json"); + writeFileSync( + entry, + `const values = [1, 2, 3, 4]; +console.log(values.every((v) => v > 0)); +console.log(values.map((v) => v * 2).join(",")); +try { + console.log(values.every((v) => { + if (v > 2) throw new Error("stop at " + v); + return true; + })); +} catch (error) { + console.log("caught", (error as Error).message); +} +`, + ); + const lowered = await compile(entry, { outputKind: "ir", outDir: dir, outPath: output }); + if (!lowered.ok) throw new Error(JSON.stringify(lowered.diagnostics)); + const module = deserializeModule(readFileSync(output, "utf8")); + expect(validateModule(module)).toEqual([]); + const llvm = emitLlvmModule(module, {}); + const guards = [ + ...llvm.matchAll( + /(%t\d+) = phi i1 \[ false, %callback\.direct\d+ \], \[ true, %callback\.indirect\d+ \]/g, + ), + ]; + // The quiet every and map callbacks; not the throwing one. + expect(guards.length).toBe(2); + for (const [, guard] of guards) + expect(llvm).toMatch(new RegExp(`br i1 ${guard}, label %exc\\.t\\d+`)); + const helpers = llvm.split(/\n(?=define )/).filter((fn) => fn.includes("callback.direct")); + expect(helpers.filter((fn) => !/phi i1 \[ false, %callback\.direct/.test(fn))).toHaveLength(1); + + const exe = join(dir, "main"); + const built = await compile(entry, { outputKind: "exe", outDir: dir, outPath: exe }); + if (!built.ok) throw new Error(JSON.stringify(built.diagnostics)); + expect(execFileSync(exe, { encoding: "utf8" })).toBe("true\n2,4,6,8\ncaught stop at 3\n"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); From 143c80eb1c360c192ef2c914a9304d3350ce1495 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 20:10:43 -0500 Subject: [PATCH 027/395] Print class instances with their class name when instanceof sees unknown values --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/class-prototypes.ts | 13 +++++++ .../compiler/src/ir/builtin-signatures.ts | 3 ++ ...6991-inspect-class-name-instanceof.ts.json | 6 +++ packages/runtime/src/scr_dyn.h | 3 ++ packages/runtime/src/scr_json.c | 38 ++++++++++++------- .../6991-inspect-class-name-instanceof.ts | 31 +++++++++++++++ ...6991-inspect-class-name-instanceof.ts.json | 1 + 8 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6991-inspect-class-name-instanceof.ts.json create mode 100644 tests/corpus/6991-inspect-class-name-instanceof.ts create mode 100644 tests/harness/corpus-costs/6991-inspect-class-name-instanceof.ts.json diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index b7c6b8a219..8bb8bc3aab 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1493,6 +1493,7 @@ export const LIB_FN_SYMS: Record = { "dyn.bagGet": "scr_dyn_bag_get", "dyn.bagSet": "scr_dyn_bag_set", "dyn.objCreate": "scr_dyn_obj_create", + "dyn.nameClassPrototype": "scr_dyn_name_class_prototype", "dyn.objCreateWithProperties": "scr_dyn_obj_create_with_properties", "dyn.getPrototype": "scr_dyn_get_prototype", "dyn.setPrototype": "scr_dyn_set_prototype", diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index d2919e2022..e65c679c6f 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -6,6 +6,7 @@ import { DYN, STRING, SYMBOL_T, + VOID, isUnitType, type IrExpr, type IrFunction, @@ -155,6 +156,18 @@ export function classPrototypeData( : { kind: "dynObjLit", fields: [], type: DYN, loc }, loc, }, + // util.inspect prints an instance with its class's name. + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.nameClassPrototype", + args: [value, { kind: "strLit", value: className, type: STRING, loc }], + type: VOID, + loc, + }, + loc, + }, { kind: "assign", localId: readyId, diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index e4bd83db72..b79b5ebe92 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3666,6 +3666,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.bagGet": { argTypes: [DYN, STRING, DYN], result: DYN }, "dyn.bagSet": { argTypes: [DYN, STRING, DYN, DYN], result: VOID }, "dyn.objCreate": { argTypes: [DYN], result: DYN }, + // Names a compiled class's prototype object (args[0]) after the class + // (args[1]) for util.inspect. + "dyn.nameClassPrototype": { argTypes: [DYN, STRING], result: VOID }, "dyn.objCreateWithProperties": { argTypes: [DYN, DYN], result: DYN }, "dyn.getPrototype": { argTypes: [DYN], result: DYN }, "dyn.setPrototype": { argTypes: [DYN, DYN], result: DYN }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6991-inspect-class-name-instanceof.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6991-inspect-class-name-instanceof.ts.json new file mode 100644 index 0000000000..a6629e2a99 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6991-inspect-class-name-instanceof.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6991-inspect-class-name-instanceof.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index d36c0d2633..37e9eacceb 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -997,6 +997,9 @@ ScrDyn *scr_dyn_get_own_property_descriptor(ScrDyn *target, ScrDyn *key); ScrDyn *scr_dyn_array_proto_call(ScrDyn *target, ScrStr *method, ScrDyn *args); ScrDyn *scr_dyn_array_prototype(void); /* +1, installs native method adapters */ ScrDyn *scr_dyn_array_prototype_base(void); /* +1, shared native array identity */ +/* Names the prototype object of a compiled class `name` for util.inspect + * (Node prints the constructor's name), unless it already has a name. */ +void scr_dyn_name_class_prototype(ScrDyn *prototype, const ScrStr *name); /* Native arrays read their holes as undefined without consulting * Array.prototype, so storing an element on Array.prototype raises SC2020. * True (with the error pending) when `recv` is Array.prototype. */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 21ccfaa168..2787fe7282 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4669,6 +4669,24 @@ static void scr_dyn_named_prototypes_cleanup(void) { scr_dyn_named_count = scr_dyn_named_cap = 0; } +/* Stamps prototype `p` with a copy of `name` (len bytes) and keeps it alive. */ +static void scr_dyn_stamp_prototype_name(ScrDyn *p, const char *name, size_t len) { + if (scr_dyn_named_count == scr_dyn_named_cap) { + if (scr_dyn_named_cap == 0) scr_atexit(scr_dyn_named_prototypes_cleanup); + size_t cap = scr_dyn_named_cap ? scr_dyn_named_cap * 2 : 8; + ScrDynNamedPrototype *grown = realloc(scr_dyn_named_prototypes, cap * sizeof *grown); + if (!grown) scr_json_oom(); + scr_dyn_named_prototypes = grown; + scr_dyn_named_cap = cap; + } + char *copy = malloc(len + 1); + if (!copy) scr_json_oom(); + memcpy(copy, name, len); + copy[len] = '\0'; + p->v.obj.class_name = copy; + scr_dyn_named_prototypes[scr_dyn_named_count++] = (ScrDynNamedPrototype){ scr_dyn_retain(p), copy }; +} + static void scr_dyn_name_prototype_chain(const ScrDyn *object) { for (ScrDyn *p = object->prototype; p; p = p->prototype) { if (p->kind != SCR_DYN_OBJ || p->v.obj.class_name) return; @@ -4676,24 +4694,16 @@ static void scr_dyn_name_prototype_chain(const ScrDyn *object) { if (!entry || entry->accessor || !entry->value || entry->value->kind != SCR_DYN_FUNC) continue; const char *name = entry->value->v.fn.name; if (!name || !name[0]) continue; - if (scr_dyn_named_count == scr_dyn_named_cap) { - if (scr_dyn_named_cap == 0) scr_atexit(scr_dyn_named_prototypes_cleanup); - size_t cap = scr_dyn_named_cap ? scr_dyn_named_cap * 2 : 8; - ScrDynNamedPrototype *grown = realloc(scr_dyn_named_prototypes, cap * sizeof *grown); - if (!grown) scr_json_oom(); - scr_dyn_named_prototypes = grown; - scr_dyn_named_cap = cap; - } - size_t len = strlen(name); - char *copy = malloc(len + 1); - if (!copy) scr_json_oom(); - memcpy(copy, name, len + 1); - p->v.obj.class_name = copy; - scr_dyn_named_prototypes[scr_dyn_named_count++] = (ScrDynNamedPrototype){ scr_dyn_retain(p), copy }; + scr_dyn_stamp_prototype_name(p, name, strlen(name)); return; } } +void scr_dyn_name_class_prototype(ScrDyn *prototype, const ScrStr *name) { + if (prototype->kind != SCR_DYN_OBJ || prototype->v.obj.class_name || name->len == 0) return; + scr_dyn_stamp_prototype_name(prototype, name->data, name->len); +} + /* Ordinary native function constructors retain their live prototype table. * A constructor may replace the fresh receiver with another object. */ ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) { diff --git a/tests/corpus/6991-inspect-class-name-instanceof.ts b/tests/corpus/6991-inspect-class-name-instanceof.ts new file mode 100644 index 0000000000..5354048c9c --- /dev/null +++ b/tests/corpus/6991-inspect-class-name-instanceof.ts @@ -0,0 +1,31 @@ +// util.inspect prints a class instance with its class's name, also when +// the program tests `instanceof` on unknown values (which gives the class +// a runtime prototype object). + +class Leaf { + x = 1; + twin: Leaf | undefined = undefined; +} +class Branch extends Leaf { + label = "b"; + describe(): string { + return `branch ${this.label}`; + } +} +class Plain { + y = 2; +} + +const solo = new Leaf(); +const branch = new Branch(); +console.log(solo); +console.log(branch, branch.describe()); +console.log([solo, branch], { solo }); +console.log(new Plain()); + +const boxed: unknown[] = [solo, branch, new Plain(), 3]; +console.log(boxed.map((v) => (v instanceof Leaf ? "L" : String(v instanceof Plain))).join(",")); +console.log(boxed.map((v) => (v instanceof Branch ? "B" : "-")).join(",")); + +solo.twin = branch; +console.log(solo); diff --git a/tests/harness/corpus-costs/6991-inspect-class-name-instanceof.ts.json b/tests/harness/corpus-costs/6991-inspect-class-name-instanceof.ts.json new file mode 100644 index 0000000000..b39c9d9734 --- /dev/null +++ b/tests/harness/corpus-costs/6991-inspect-class-name-instanceof.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 5400 } From 9b165aa0449778fa57fa5715c04cb8e4ca026148 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 20:12:26 -0500 Subject: [PATCH 028/395] Lower a super() call that leads an if condition's comma chain --- .../src/frontend/lowering/lower-classes.ts | 27 ++++++++++++ .../src/frontend/lowering/lower-stmts.ts | 29 +++++++------ ...6944-js-super-call-in-if-condition.js.json | 6 +++ .../6944-js-super-call-in-if-condition.js | 41 +++++++++++++++++++ ...6944-js-super-call-in-if-condition.js.json | 1 + 5 files changed, 92 insertions(+), 12 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6944-js-super-call-in-if-condition.js.json create mode 100644 tests/corpus/6944-js-super-call-in-if-condition.js create mode 100644 tests/harness/corpus-costs/6944-js-super-call-in-if-condition.js.json diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 17b22f0d48..790efe988d 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -8538,6 +8538,10 @@ function superCallInStatementPosition(call: ts.CallExpression): boolean { for (;;) { const parent = node.parent; if (ts.isExpressionStatement(parent)) return true; + if (ts.isIfStatement(parent)) { + const lead = superConditionLead(parent.expression); + return lead !== null && call.pos >= lead.left.pos && call.end <= lead.left.end; + } const comma = ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.CommaToken; if (!comma && !ts.isParenthesizedExpression(parent)) return false; @@ -8545,6 +8549,29 @@ function superCallInStatementPosition(call: ts.CallExpression): boolean { } } +/** The comma chain of an `if` condition whose leading operands hold a + * super() call (`if (super(), r === 0) throw ...`, as minifiers join a + * super statement with the next test), or null. The if statement runs the + * chain's left operand as a statement first and tests its right operand, + * which is what the comma evaluates. */ +export function superConditionLead(cond: ts.Expression): ts.BinaryExpression | null { + let expr = cond; + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if (!ts.isBinaryExpression(expr) || expr.operatorToken.kind !== ts.SyntaxKind.CommaToken) + return null; + const holdsSuperCall = (operand: ts.Expression): boolean => { + let e = operand; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (ts.isCallExpression(e)) return e.expression.kind === ts.SyntaxKind.SuperKeyword; + return ( + ts.isBinaryExpression(e) && + e.operatorToken.kind === ts.SyntaxKind.CommaToken && + (holdsSuperCall(e.left) || holdsSuperCall(e.right)) + ); + }; + return holdsSuperCall(expr.left) ? expr : null; +} + /** Super statements initialize the base and then the derived fields on * whichever control-flow path executes. Repeated calls are explicitly * refused until native layouts can allocate a second receiver. */ diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 78f2f4189e..e3480cf9c8 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -159,7 +159,7 @@ import { classStaticDataFor } from "./class-static-data.js"; import { objectFactorySignature } from "./object-factory-new.js"; import { isClassCallback, lowerClassCallbackAssign } from "./class-callbacks.js"; import { classForInHasKey, classPropertiesHelper } from "./class-dynamic-dispatch.js"; -import { genericIfaceBindingKeepsClass } from "./lower-classes.js"; +import { genericIfaceBindingKeepsClass, superConditionLead } from "./lower-classes.js"; import { lowerStreamUnderscoreAssign, streamClassAliasDecl } from "./lower-stream.js"; import { staticAssertInstanceDecl } from "./assert-instances.js"; import { @@ -1537,43 +1537,48 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt return lowerer.lowerDefaultExport(stmt); } if (ts.isIfStatement(stmt)) { - const cond = lowerer.lowerCondition(stmt.expression); + // `if (super(), test)`: the super call runs as a statement, then the + // test decides. + const lead = superConditionLead(stmt.expression); + const leading = lead ? lowerer.lowerExprStatement(lead.left) : null; + const test = lead ? lead.right : stmt.expression; + const cond = lowerer.lowerCondition(test); // A global-detection condition (static-typeof.ts) folds; its untaken // arm is code Node never runs and does not lower, unless it declares a // hoisted binding. - const decided = staticCondition(lowerer, stmt.expression); + const decided = staticCondition(lowerer, test); if (decided !== null) { const taken = decided ? stmt.thenStatement : stmt.elseStatement; const untaken = decided ? stmt.elseStatement : stmt.thenStatement; if (untaken === undefined || !declaresHoistedBinding(untaken)) { const arm = taken === undefined ? [] : lowerer.lowerScopedBlock(taken); - return decided + const folded: IrStmt = decided ? { kind: "if", cond, then: arm, else_: null, loc: locOf(stmt) } : { kind: "if", cond, then: [], else_: arm, loc: locOf(stmt) }; + return leading ? { kind: "block", body: [leading, folded], loc: locOf(stmt) } : folded; } } // Aliased-typeof narrows (ms's `var type = typeof val` guard): the // then-branch lowers under what a TRUE condition proves, the // else-branch under a FALSE one — the var/let alias narrowing the // checker only performs for consts (aliasTypeofNarrows). - const then = lowerer.narrowingAliases(aliasTypeofNarrows(lowerer, stmt.expression, true), () => - withRuntimeOptionalNarrowed(lowerer, runtimeOptionalTrueIds(lowerer, stmt.expression), () => + const then = lowerer.narrowingAliases(aliasTypeofNarrows(lowerer, test, true), () => + withRuntimeOptionalNarrowed(lowerer, runtimeOptionalTrueIds(lowerer, test), () => lowerer.lowerScopedBlock(stmt.thenStatement), ), ); const else_ = stmt.elseStatement - ? lowerer.narrowingAliases(aliasTypeofNarrows(lowerer, stmt.expression, false), () => - withRuntimeOptionalNarrowed( - lowerer, - runtimeOptionalGuardIds(lowerer, stmt.expression, false), - () => lowerer.lowerScopedBlock(stmt.elseStatement!), + ? lowerer.narrowingAliases(aliasTypeofNarrows(lowerer, test, false), () => + withRuntimeOptionalNarrowed(lowerer, runtimeOptionalGuardIds(lowerer, test, false), () => + lowerer.lowerScopedBlock(stmt.elseStatement!), ), ) : null; for (const local of falsyExitRuntimeOptionalLocals(lowerer, stmt)) { lowerer.runtimeOptionalLocals.delete(lowerer.runtimeOptionalRootOf(local)); } - return { kind: "if", cond, then, else_, loc: locOf(stmt) }; + const lowered: IrStmt = { kind: "if", cond, then, else_, loc: locOf(stmt) }; + return leading ? { kind: "block", body: [leading, lowered], loc: locOf(stmt) } : lowered; } if (ts.isWhileStatement(stmt)) { const labels = lowerer.takeLabels(); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6944-js-super-call-in-if-condition.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6944-js-super-call-in-if-condition.js.json new file mode 100644 index 0000000000..ba20299f2c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6944-js-super-call-in-if-condition.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6944-js-super-call-in-if-condition.js" + ], + "diags": [] +} diff --git a/tests/corpus/6944-js-super-call-in-if-condition.js b/tests/corpus/6944-js-super-call-in-if-condition.js new file mode 100644 index 0000000000..8fdc8022b0 --- /dev/null +++ b/tests/corpus/6944-js-super-call-in-if-condition.js @@ -0,0 +1,41 @@ +// A derived constructor whose super() call leads an if condition's comma +// chain, as minifiers join `super(...); if (test) ...`: the base and the +// derived fields initialize first, then the test decides. + +let count = 0; +class Base { + constructor(name) { + this.name = name; + this.created = count++; + } +} + +class Checked extends Base { + limit = 3; + constructor(name, size) { + if ((super(name), size > this.limit)) throw new RangeError(`${name}: ${size} > ${this.limit}`); + else this.size = size; + } +} + +class Tagged extends Base { + constructor(name, tag) { + if ((super(name), (this.tag = tag), tag === undefined)) this.tag = "none"; + } +} + +class Plain extends Base { + constructor(name, flag) { + if ((super(name), flag)) this.flag = "set"; + this.done = true; + } +} + +const made = [new Checked("a", 2), new Tagged("b", "x"), new Tagged("c"), new Plain("d", 1)]; +for (const value of made) console.log(JSON.stringify(value)); +try { + new Checked("e", 9); +} catch (error) { + console.log(error.name, error.message, count); +} +console.log(new Plain("f", 0).flag, count); diff --git a/tests/harness/corpus-costs/6944-js-super-call-in-if-condition.js.json b/tests/harness/corpus-costs/6944-js-super-call-in-if-condition.js.json new file mode 100644 index 0000000000..5904670c9a --- /dev/null +++ b/tests/harness/corpus-costs/6944-js-super-call-in-if-condition.js.json @@ -0,0 +1 @@ +{ "plain": 1700, "san": 2600 } From 6f0d047e8ea39b8cf9e89715917145e82ee90517 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 20:21:31 -0500 Subject: [PATCH 029/395] Pass a reassigned zlib callback result on to typed functions On the error path, a callback's result reads from a checked-dynamic slot. Passing it on into a Buffer parameter still handed over the adapter's original value, so after `if (!out) out = Buffer.from(...)` a typed callee received the absent Buffer instead of the fallback and crashed. The forward now tests the slot: while it holds undefined the callee gets the absent Buffer, and otherwise the Buffer the slot holds. --- .../src/frontend/lowering/lower-functions.ts | 65 ++++++++++++------- .../corpus/6920-zlib-callback-error-result.ts | 13 +++- 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 8f2c575a4d..cd8dc31d1d 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -751,31 +751,35 @@ function lowerUndefinedResultBody( lowerer.suppressStats = true; lowerer.errorPathCopies++; try { - // Passed on into a Buffer slot, the result stays what the adapter - // passed: the Buffer, or the absent Buffer, which a callee with this - // entry test reads as undefined. (The copy that serves both paths - // reaches this with a Buffer.) - const body = transformStmtList(withKnownError(knownError, lowerBody), { - expr: (e) => - e.kind === "dynCheck" && - e.value.kind === "varRef" && - e.value.localId === slot.id && - e.type.kind === "bytes" && - e.type.elem === "u8" - ? { - kind: "ternary", - cond: absent(), - then: { kind: "libCall", fn: "bytes.absent", args: [], type: e.type, loc: e.loc }, - else_: varRef(param.id, param.type, e.loc), - type: e.type, - loc: e.loc, - } - : e, + const lowered = withKnownError(knownError, lowerBody); + if (lowerer.diags.length > diags || !readsSlotDynamically(lowered, slot.id)) + throw new PoisonError(); + // Passed on into a Buffer slot, the result is the Buffer it holds + // (the adapter's, or one the body assigned), or, while it holds + // undefined, the absent Buffer, which a callee with this entry test + // reads as undefined. + const guarded = new Set(); + return transformStmtList(lowered, { + expr: (e) => { + if (!isBufferCheckOf(e, slot.id) || guarded.has(e)) return e; + guarded.add(e); + return { + kind: "ternary", + cond: { + kind: "dynTest", + test: "undefined", + value: varRef(slot.id, DYN, e.loc), + type: BOOL, + loc: e.loc, + }, + then: { kind: "libCall", fn: "bytes.absent", args: [], type: e.type, loc: e.loc }, + else_: e, + type: e.type, + loc: e.loc, + }; + }, stmt: (stmt) => stmt, }); - if (lowerer.diags.length > diags || !readsSlotDynamically(body, slot.id)) - throw new PoisonError(); - return body; } catch (error) { if (error instanceof InternalCompilerError) throw error; lowerer.diags.length = diags; @@ -831,15 +835,26 @@ function lowerUndefinedResultBody( /** Whether the error-path copy reads the checked-dynamic result slot only as * a checked-dynamic value, never converted into a static slot (where Node - * would hold undefined). */ + * would hold undefined) other than a Buffer one. */ function readsSlotDynamically(body: IrStmt[], slotId: string): boolean { const isSlot = (e: IrExpr): boolean => e.kind === "varRef" && e.localId === slotId; return everyStmtList(body, { expr: (e) => { if (isSlot(e)) return e.type.kind === "dyn"; - if (e.kind === "dynCheck" && isSlot(e.value)) return false; + if (e.kind === "dynCheck" && isSlot(e.value)) return isBufferCheckOf(e, slotId); return true; }, stmt: () => true, }); } + +/** Whether `e` converts the result slot into a Buffer slot. */ +function isBufferCheckOf(e: IrExpr, slotId: string): boolean { + return ( + e.kind === "dynCheck" && + e.value.kind === "varRef" && + e.value.localId === slotId && + e.type.kind === "bytes" && + e.type.elem === "u8" + ); +} diff --git a/tests/corpus/6920-zlib-callback-error-result.ts b/tests/corpus/6920-zlib-callback-error-result.ts index 5016fa8418..44803333ba 100644 --- a/tests/corpus/6920-zlib-callback-error-result.ts +++ b/tests/corpus/6920-zlib-callback-error-result.ts @@ -176,15 +176,24 @@ function namedRetry(): void { }); } +function shown(label: string, out: Buffer): void { + console.log(label, typeof out, out.toString()); +} function fallbackResult(): void { - // The body assigns a fallback Buffer to its result. + // The body assigns a fallback Buffer to its result, then passes the + // result on to typed functions. zlib.inflate(bad, (err, out) => { if (!out) out = Buffer.from("fallback"); console.log("fallback", err?.code, typeof out, out.toString()); + shown("fallback passed", out); zlib.inflate(good, (err2, out2) => { if (err2) out2 = Buffer.alloc(0); console.log("fallback nested", err2, out2.length); - step(); + shown("fallback nested passed", out2); + zlib.inflate(bad, (err3, out3) => { + if (err3) out3 = Buffer.from("inner fallback"); + sink(null, out3); + }); }); }); } From 37cdd927557a56750581c74561bcc86f34aa55f9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 20:23:53 -0500 Subject: [PATCH 030/395] Keep properties untyped code adds to typed object literals --- .../src/backend/llvm/expr-stream-bridges.ts | 7 ++ .../6990-record-writes-from-js/main.ts.json | 7 ++ packages/runtime/src/scr_dyn.h | 5 ++ packages/runtime/src/scr_json.c | 77 ++++++++++++++++--- .../6990-record-writes-from-js/helpers.js | 13 ++++ .../corpus/6990-record-writes-from-js/main.ts | 23 ++++++ .../6990-record-writes-from-js/main.ts.json | 1 + 7 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6990-record-writes-from-js/main.ts.json create mode 100644 tests/corpus/6990-record-writes-from-js/helpers.js create mode 100644 tests/corpus/6990-record-writes-from-js/main.ts create mode 100644 tests/harness/corpus-costs/6990-record-writes-from-js/main.ts.json diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index b72875aaa8..a4f2ed749e 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -36,6 +36,7 @@ import type { LlvmEmitterContext, LlStreamTypedRefAdapter } from "./expr-context 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"; /** 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 @@ -539,6 +540,12 @@ export function streamTypedRefCommitAdapter( ` store ${member.type} %old${index}, ptr %np${index}`, ); }); + // The conversion kept the view's undeclared properties for %next. Shapes + // with accessor slots keep their accessors in the view instead. + if (recordMayKeepSource(shape) && !shapeHasAccessorSlots(shape)) { + host.declare(`declare void @scr_record_source_move(ptr, ptr)`); + lines.push(` call void @scr_record_source_move(ptr %next, ptr %target)`); + } lines.push( ` call void ${releaseSym(host.shapeHost, t)}(ptr %next)`, ` br label %done`, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6990-record-writes-from-js/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6990-record-writes-from-js/main.ts.json new file mode 100644 index 0000000000..8eeecc9dda --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6990-record-writes-from-js/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6990-record-writes-from-js/helpers.js", + "/tests/corpus/6990-record-writes-from-js/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 37e9eacceb..02c4d828f2 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -892,6 +892,11 @@ void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops); void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *names, const size_t *lens, size_t count); bool scr_record_source_has(const void *rec); +/* The commit of a live record view converts the view into a new record + * `from`, whose conversion keeps the view's undeclared properties, then + * moves its fields into `to`: this moves the kept properties to `to` too, + * replacing any `to` had. */ +void scr_record_source_move(void *from, void *to); ScrDyn *scr_record_source_view(const void *rec); ScrDyn *scr_record_source_apply(ScrDyn *snapshot, const void *rec); /* Attach the prototype member names of a class instance capsule diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 2787fe7282..bd9bc2cc29 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2724,20 +2724,24 @@ bool scr_record_source_has(const void *rec) { return scr_record_source_count && scr_record_source_find(rec) != NULL; } -static void scr_record_source_disposed(void *rec) { - size_t bit = scr_record_source_filter_bit(rec); - if (!(scr_record_source_filter[bit >> 6] & (UINT64_C(1) << (bit & 63)))) return; - ScrRecordSource *found = scr_record_source_find(rec); - if (!found) return; +/* Drops the entry at `found` from the table, releasing its view. The table + * no longer refers to the view first, so a nested disposal that re-enters + * the dispose hook sees a consistent table. */ +static void scr_record_source_drop(ScrRecordSource *found) { ScrDyn *view = found->view; *found = (ScrRecordSource){ .rec = SCR_RECORD_SOURCE_DELETED }; scr_record_source_count--; scr_record_source_deleted++; - /* The table no longer refers to the view, so a nested disposal that - * re-enters this hook sees a consistent table. */ scr_dyn_release(view); } +static void scr_record_source_disposed(void *rec) { + size_t bit = scr_record_source_filter_bit(rec); + if (!(scr_record_source_filter[bit >> 6] & (UINT64_C(1) << (bit & 63)))) return; + ScrRecordSource *found = scr_record_source_find(rec); + if (found) scr_record_source_drop(found); +} + /* The single native dispose hook serves weak collections and the table * above; each observer runs only while it has entries to forget. */ static void scr_dispose_observers(void *object) { @@ -2878,6 +2882,20 @@ void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *nam scr_record_source_put(&probe); } +void scr_record_source_move(void *from, void *to) { + if (!scr_record_source_count) return; + ScrRecordSource *old = scr_record_source_find(to); + if (old) scr_record_source_drop(old); + ScrRecordSource *found = scr_record_source_find(from); + if (!found) return; + ScrRecordSource moved = *found; + *found = (ScrRecordSource){ .rec = SCR_RECORD_SOURCE_DELETED }; + scr_record_source_count--; + scr_record_source_deleted++; + moved.rec = to; + scr_record_source_put(&moved); +} + /* Refreshes a retained record view: the view's own-key order, declared * values from the native snapshot (a declared key the record no longer has * drops), every other property as the view holds it, then declared fields @@ -2961,6 +2979,34 @@ static bool scr_jb_put_source_field(ScrJsonBuf *b, void *rec, const ScrRecordSou /* The same in one lookup: writes the record and answers true when it kept * a source, otherwise writes nothing and answers false. */ +static bool scr_dyn_key_is_index(const char *key, size_t len, double *out); + +/* The entry indexes of `view` in JavaScript's own-key order: integer keys + * ascending, then the other keys in insertion order. `out` holds len. */ +static void scr_record_source_key_order(const ScrDyn *view, size_t *out) { + size_t integers = 0; + for (size_t i = 0; i < view->v.obj.len; i++) { + double index; + const ScrDynEntry *entry = &view->v.obj.entries[i]; + if (!scr_dyn_key_is_index(entry->key, entry->key_len, &index)) continue; + size_t at = integers++; + for (; at > 0; at--) { + double before; + const ScrDynEntry *previous = &view->v.obj.entries[out[at - 1]]; + scr_dyn_key_is_index(previous->key, previous->key_len, &before); + if (before < index) break; + out[at] = out[at - 1]; + } + out[at] = i; + } + size_t next = integers; + for (size_t i = 0; i < view->v.obj.len; i++) { + double index; + const ScrDynEntry *entry = &view->v.obj.entries[i]; + if (!scr_dyn_key_is_index(entry->key, entry->key_len, &index)) out[next++] = i; + } +} + bool scr_jb_try_record_source(ScrJsonBuf *b, void *rec, bool (*field)(ScrJsonBuf *, void *, size_t, bool)) { ScrRecordSource *found = scr_record_source_find(rec); @@ -2981,13 +3027,24 @@ bool scr_jb_try_record_source(ScrJsonBuf *b, void *rec, bool small[64] = { false }; bool *seen = count <= 64 ? small : calloc(count, sizeof *seen); if (!seen) scr_json_oom(); + /* Integer keys come first in JavaScript's own-key order; a view without + * any walks in insertion order. */ + size_t order_small[64]; + size_t *order = NULL; + for (size_t k = 0; k < view->v.obj.len && !order; k++) { + double index; + if (!scr_dyn_key_is_index(view->v.obj.entries[k].key, view->v.obj.entries[k].key_len, &index)) continue; + order = view->v.obj.len <= 64 ? order_small : malloc(view->v.obj.len * sizeof *order); + if (!order) scr_json_oom(); + scr_record_source_key_order(view, order); + } /* Members whose writing cannot run program code are written in place. * The first member that can (an accessor, a toJSON method, a getter below * it) ends that loop: the remaining keys are taken first, as * SerializeJSONObject takes them before reading any value. */ size_t i = 0; for (; i < view->v.obj.len && !scr_exc_pending(); i++) { - const ScrDynEntry *entry = &view->v.obj.entries[i]; + const ScrDynEntry *entry = &view->v.obj.entries[order ? order[i] : i]; size_t index = scr_record_source_index(&source, entry->key, entry->key_len); if (index < count) { if (!seen[index] && scr_jb_put_source_field(b, rec, &source, index, first, on_stack, field)) @@ -3007,7 +3064,7 @@ bool scr_jb_try_record_source(ScrJsonBuf *b, void *rec, if (i < view->v.obj.len && !scr_exc_pending()) { ScrDyn *rest = scr_dyn_new_arr(); for (size_t j = i; j < view->v.obj.len; j++) { - const ScrDynEntry *entry = &view->v.obj.entries[j]; + const ScrDynEntry *entry = &view->v.obj.entries[order ? order[j] : j]; if (!entry->enumerable && !scr_record_source_declared(&source, entry->key, entry->key_len)) continue; ScrStr *name = scr_str_new(entry->key, entry->key_len); scr_dyn_arr_push(rest, scr_dyn_new_str(name)); @@ -3032,6 +3089,7 @@ bool scr_jb_try_record_source(ScrJsonBuf *b, void *rec, first = false; } if (seen != small) free(seen); + if (order && order != order_small) free(order); scr_dyn_release(view); scr_jb_putc(b, '}'); if (entered) scr_jb_leave(b); @@ -4552,7 +4610,6 @@ bool scr_dyn_same_value(const ScrDyn *left, const ScrDyn *right) { static void scr_dyn_descriptor_fields_drop(ScrDyn **fields) { for (size_t i = 0; i < 6; i++) scr_dyn_release(fields[i]); } -static bool scr_dyn_key_is_index(const char *key, size_t len, double *out); static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key); static void scr_error_sync_slots(const ScrDyn *view, ScrDyn *obj, const ScrStr *key); diff --git a/tests/corpus/6990-record-writes-from-js/helpers.js b/tests/corpus/6990-record-writes-from-js/helpers.js new file mode 100644 index 0000000000..6f43a90ce3 --- /dev/null +++ b/tests/corpus/6990-record-writes-from-js/helpers.js @@ -0,0 +1,13 @@ +// Untyped helpers that add, replace and remove properties of the objects +// they receive. +export function write(target, key, value) { + target[key] = value; +} + +export function remove(target, key) { + delete target[key]; +} + +export function keysOf(target) { + return Object.keys(target).join(","); +} diff --git a/tests/corpus/6990-record-writes-from-js/main.ts b/tests/corpus/6990-record-writes-from-js/main.ts new file mode 100644 index 0000000000..2b343efc5f --- /dev/null +++ b/tests/corpus/6990-record-writes-from-js/main.ts @@ -0,0 +1,23 @@ +// A typed object literal passed to untyped JavaScript keeps the properties +// that code adds: JSON.stringify, Object.keys, later untyped reads and +// console.log see them, in JavaScript's own-key order (integer keys first). +import { keysOf, remove, write } from "./helpers.js"; + +const numbered = { 0: "zero", 1.5: "x" }; +write(numbered, 2, "two"); +console.log(JSON.stringify(numbered), keysOf(numbered), Object.keys(numbered)); + +const point = { x: 1, y: 2 }; +write(point, "label", "origin"); +write(point, "x", 10); +console.log(JSON.stringify(point), point.x, keysOf(point)); +write(point, "label", "moved"); +console.log(JSON.stringify(point)); +remove(point, "label"); +console.log(JSON.stringify(point), keysOf(point)); + +const config = { name: "app", retries: 3 }; +write(config, "debug", true); +write(config, "tags", ["a", "b"]); +console.log(JSON.stringify(config), keysOf(config)); +console.log(config); diff --git a/tests/harness/corpus-costs/6990-record-writes-from-js/main.ts.json b/tests/harness/corpus-costs/6990-record-writes-from-js/main.ts.json new file mode 100644 index 0000000000..b8b8cc39d8 --- /dev/null +++ b/tests/harness/corpus-costs/6990-record-writes-from-js/main.ts.json @@ -0,0 +1 @@ +{ "plain": 4300, "san": 3700 } From 3b5e23361805adb155264c016f091b9799d3dd72 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 20:28:16 -0500 Subject: [PATCH 031/395] Lower to the same IR whatever order reachable bodies lower in --- package.json | 1 + .../lowering/caught-value-dispatch.ts | 47 +- .../lowering/class-dynamic-dispatch.test.ts | 1 + .../lowering/class-dynamic-dispatch.ts | 19 + .../src/frontend/lowering/class-prototypes.ts | 1 + .../src/frontend/lowering/error-methods.ts | 4 + .../lowering/generator-dynamic-dispatch.ts | 3 + .../frontend/lowering/lower-event-emitter.ts | 8 +- .../src/frontend/lowering/lower-modules.ts | 3 +- .../compiler/src/frontend/lowering/lowerer.ts | 166 +++++- .../test/format-float-arguments.test.ts | 5 +- scripts/lowering-order.mjs | 564 ++++++++++++++++++ 12 files changed, 792 insertions(+), 30 deletions(-) create mode 100644 scripts/lowering-order.mjs diff --git a/package.json b/package.json index ff4c84dc26..5cc30d9f80 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "test:test262": "tsx tests/test262/run.ts", "test:test262:fetch": "node tests/test262/fetch.mjs", "test:cache-identity": "node tests/harness/cache-identity.mjs", + "test:lowering-order": "node scripts/lowering-order.mjs --fail-on content", "test:sandbox": "node scripts/sandbox-test.mjs", "test:sandbox:image": "node scripts/sandbox-image.mjs", "test:watch": "vitest", diff --git a/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts b/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts index 645ff8d2cc..606d10d563 100644 --- a/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts @@ -5,6 +5,7 @@ import { DYN, type IrExpr, type IrFunction, + type IrStmt, type IrType, type SrcLoc, } from "../../ir/ir.js"; @@ -59,28 +60,48 @@ export class CaughtValueDispatch { loc, }; lowerer.liftedFns.push(this.helper); + lowerer.shareHelper(this.helper); changed = true; } const helper = this.helper; + let added = false; for (const className of this.thrownClasses) { if (this.convertedClasses.has(className)) continue; this.convertedClasses.add(className); + added = true; + } + if (added) { + // A subclass is tested before its ancestors, and classes of one depth + // by name, so the tests do not depend on the order the census met them. + const depth = (className: string): number => { + let count = 0; + for (let info = lowerer.classes.get(className)?.base; info; info = info.base) count++; + return count; + }; + const ordered = [...this.convertedClasses].sort( + (left, right) => depth(right) - depth(left) || (left < right ? -1 : left > right ? 1 : 0), + ); const loc = helper.loc; const value = varRef("caught", CAUGHT, loc); - const type: IrType = { kind: "object", className }; - helper.body.unshift({ - kind: "if", - cond: { kind: "caughtTest", value, test: "instanceof", className, type: BOOL, loc }, - then: [ - { - kind: "return", - value: lowerer.coerceToExpected({ kind: "caughtNarrow", value, type, loc }, DYN), + helper.body = [ + ...ordered.map((className): IrStmt => { + const type: IrType = { kind: "object", className }; + return { + kind: "if", + cond: { kind: "caughtTest", value, test: "instanceof", className, type: BOOL, loc }, + then: [ + { + kind: "return", + value: lowerer.coerceToExpected({ kind: "caughtNarrow", value, type, loc }, DYN), + loc, + }, + ], + else_: null, loc, - }, - ], - else_: null, - loc, - }); + }; + }), + helper.body[helper.body.length - 1]!, + ]; changed = true; } for (const fn of consumers) { diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts index 6923841bd2..b717452c2d 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts @@ -33,6 +33,7 @@ function context() { classes: new Map(), liftedFns, liftedFnNamed: (name: string | undefined) => liftedFns.find((fn) => fn.name === name), + shareHelper: () => {}, subclassLinks: 0, constructedClassCount: () => -1, diags: [], diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 9dd7681a64..a41751f018 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -281,6 +281,7 @@ export function classInstanceOf( classValue?: IrExpr, ): IrExpr { const name = `%dyn.class.${classValue ? "valueInstanceof" : "instanceof"}:${info.def.name}`; + lowerer.shareHelper(name); const params = [ { localId: "value", name: "value", type: DYN }, ...(classValue ? [{ localId: "class", name: "class", type: classValue.type }] : []), @@ -406,8 +407,16 @@ function withTypedReceiver(value: IrExpr, receiver: IrExpr): IrExpr { }); } +/** Re-adds the entries of `set` after the first `from` sorted. */ +function sortAdded(set: Set, from: number): void { + const added = [...set].slice(from).sort(); + for (const name of added) set.delete(name); + for (const name of added) set.add(name); +} + export function classPropertiesHelper(lowerer: Lowerer, loc: SrcLoc, readOnly = false): IrFunction { const name = readOnly ? READ_PROPERTIES : "%dyn.class.properties"; + lowerer.shareHelper(name); const existing = lowerer.liftedFnNamed(name); if (existing) return existing; const helper: IrFunction = { @@ -447,6 +456,8 @@ export function classForInHasKey( loc: SrcLoc, ): IrExpr { const name = "%dyn.class.forInHasKey"; + lowerer.shareHelper(name); + lowerer.shareHelper("%dyn.class.forInBagHas"); if (!lowerer.liftedFns.some((fn) => fn.name === name)) { // The read-only table: a recheck never creates an instance's table, which // is undefined while the instance has no own dynamic properties. @@ -825,6 +836,8 @@ export class ClassDynamicDispatch { else if (type.kind === "func") discover(type.ret); else if (type.kind === "promise") discover(type.inner); }; + const boxedBefore = this.boxed.size; + const boxedConstructorsBefore = this.boxedConstructors.size; for (const fn of functions) everyStmtList(fn.body, { stmt: (statement) => { @@ -919,6 +932,10 @@ export class ClassDynamicDispatch { return true; }, }); + // The classes this pass found join the tables by name, not in the order + // the scan met them. + sortAdded(this.boxed, boxedBefore); + sortAdded(this.boxedConstructors, boxedConstructorsBefore); changed = this.caughtDispatch.dynamicPayloads(lowerer, caughtSites, this.boxed) || changed; if (this.boxed.size === 0 && rewrite.size === 0) return changed; const converting = this.convertingHints(lowerer); @@ -4264,6 +4281,8 @@ export class ClassDynamicDispatch { * prototype-backed bag is produced for this read only. A published object * cannot take the store, and Node's reads never write. */ finalize(lowerer?: Lowerer): void { + // Each table serves every site that reaches it. + if (lowerer) for (const fn of this.generated) lowerer.shareHelper(fn); for (const { helper, initialize, orEmpty } of this.readPropertyBags) { const absent = helper.body[0]; if (isEmptyBag(initialize.value) && !orEmpty) helper.body.shift(); diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index e65c679c6f..4cbd8e5cff 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -122,6 +122,7 @@ export function classPrototypeData( const globalId = `%g.${name}`; const readyId = `${globalId}.ready`; info.def.prototypeDataHelper = name; + lowerer.shareHelper(name); const className = info.def.jsName ?? info.def.name; lowerer.globalsList.push({ id: globalId, diff --git a/packages/compiler/src/frontend/lowering/error-methods.ts b/packages/compiler/src/frontend/lowering/error-methods.ts index 979cf4814d..b545f41476 100644 --- a/packages/compiler/src/frontend/lowering/error-methods.ts +++ b/packages/compiler/src/frontend/lowering/error-methods.ts @@ -24,6 +24,7 @@ export function errorPropertyRead( ): IrExpr { const name = `%error.${property}.read`, loc = receiver.loc; + lowerer.shareHelper(name); if (!lowerer.liftedFns.some((fn) => fn.name === name)) lowerer.liftedFns.push({ name, @@ -50,6 +51,7 @@ export function errorPropertyWrite( ): IrStmt { const name = `%error.${property}.write`, loc = receiver.loc; + lowerer.shareHelper(name); if (!lowerer.liftedFns.some((fn) => fn.name === name)) lowerer.liftedFns.push({ name, @@ -392,6 +394,7 @@ export function errorToStringMethod( receiver: IrExpr, ): Extract { const loc = receiver.loc; + lowerer.shareHelper(helperName); if (!lowerer.liftedFns.some((fn) => fn.name === helperName)) { lowerer.liftedFns.push({ name: helperName, @@ -415,6 +418,7 @@ export function errorToStringCall(lowerer: Lowerer, receiver: IrExpr): IrExpr { const loc = receiver.loc; const method = errorToStringMethod(lowerer, receiver); const name = "%error.toString.virtual"; + lowerer.shareHelper(name); if (!lowerer.liftedFns.some((fn) => fn.name === name)) { const value = varRef("error.0", errorType, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts index ca6a5ecdce..3f43e5e051 100644 --- a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts @@ -68,8 +68,10 @@ export class GeneratorDynamicDispatch { this.properties.set(method, { fn, types: new Set() }); this.generated.add(fn); lowerer.liftedFns.push(fn); + lowerer.shareHelper(fn); // The runtime's own helper functions answer the helper methods. if (HELPER_METHODS.has(method)) return fn; + lowerer.shareHelper(`%dyn.generator.method.${method}`); lowerer.liftedFns.push({ name: `%dyn.generator.method.${method}`, params: [{ localId: "arg", name: "arg", type: DYN }], @@ -209,6 +211,7 @@ export class GeneratorDynamicDispatch { this.dispatches.set(key, dispatch); this.generated.add(helper); lowerer.liftedFns.push(helper); + lowerer.shareHelper(helper); changed = true; } return { diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index d84f84f4ee..f472672b20 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -2128,8 +2128,12 @@ function ensureEmitSpec(lowerer: Lowerer, info: ClassInfo, event: string, tuple: params: tuple.map((t) => ({ type: t, mode: "required" as const })), ret: BOOL, }); - if (info.def.methods) info.def.methods.push(mName); - else info.def.methods = [mName]; + // Specializations sit in the method list sorted by event name, so the + // class table does not depend on which body asked for one first. + const methods = (info.def.methods ??= []); + const after = methods.findIndex((m) => m.startsWith("emit:") && m > mName); + if (after < 0) methods.push(mName); + else methods.splice(after, 0, mName); } const ordinal = lowerer.emitSpecQueue.filter((q) => q.info === info).length; lowerer.emitSpecQueue.push({ info, event, tuple, ordinal }); diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 6ca21ddaaa..d1f276d44b 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -1048,7 +1048,8 @@ export function moduleArtifacts( } const reachable = lowerer.reachableForArtifacts ?? lowerer.reachable; return { - classes: [...lowerer.classes.values()] + classes: lowerer + .classesInTableOrder() .map((c) => c.def) .filter((def) => classNames.has(def.name)) .map((def): IrClassDef => { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 383143c97a..29f8073363 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -959,6 +959,33 @@ function directExternalTypeSpecifiersByFile( return out; } +/** Debugging aid: SCRIPTC_LOWER_SHUFFLE_SEED= lowers each wave of + * reachable bodies in an order drawn from that seed instead of discovery + * order. Output assembly is ordered independently of it, so any IR + * difference between seeds is state one body's lowering leaves for another + * (scripts/lowering-order.mjs). */ +function lowerShuffle(): ((wave: string[]) => void) | null { + const raw = process.env["SCRIPTC_LOWER_SHUFFLE_SEED"]; + if (raw === undefined || raw === "") return null; + // mulberry32 + let state = Number.parseInt(raw, 10) >>> 0; + const next = (): number => { + state = (state + 0x6d2b79f5) >>> 0; + let t = state; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; + return (wave) => { + for (let index = wave.length - 1; index > 0; index--) { + const other = Math.floor(next() * (index + 1)); + const swap = wave[index]!; + wave[index] = wave[other]!; + wave[other] = swap; + } + }; +} + /** Build lowering runs as a reachability worklist over the ts.Program: * * 1. REACHABLE EMIT — a worklist computes the set of reachable bodies. @@ -1841,6 +1868,13 @@ function monomorphizedImplicit( return true; } +/** `fns` sorted by name; functions of one name keep their order. */ +function byName(fns: readonly IrFunction[]): IrFunction[] { + return [...fns].sort((left, right) => + left.name < right.name ? -1 : left.name > right.name ? 1 : 0, + ); +} + export class Lowerer { readonly frontendServices: FrontendServices | undefined; readonly checker: ts.TypeChecker; @@ -2027,6 +2061,40 @@ export class Lowerer { this.genericClassInstances.splice(from, tail.length, ...tail); } + /** The classes in table order. Generic instances are added as bodies + * demand them, so they keep the slots they hold and fill them in the + * order of their settled demand ranks. */ + classesInTableOrder(): ClassInfo[] { + const all = [...this.classes.values()]; + const slots = all.flatMap((info, index) => + this.genericClassDemandPriority.has(info) ? [index] : [], + ); + const ranked = slots + .map((index) => all[index]!) + .sort((left, right) => { + const a = this.genericClassDemandPriority.get(left)!.rank; + const b = this.genericClassDemandPriority.get(right)!.rank; + return a[0]! - b[0]! || a[1]! - b[1]!; + }); + slots.forEach((slot, index) => (all[slot] = ranked[index]!)); + return all; + } + + /** The globals in table order. A class's prototype data globals are + * added when a body first reads that prototype, so they keep the slots + * they hold and fill them in id order. */ + private globalsInTableOrder(): IrGlobal[] { + const all = [...this.globalsList]; + const prototypeData = (global: IrGlobal): boolean => + global.id.startsWith("%g.%prototype.data."); + const slots = all.flatMap((global, index) => (prototypeData(global) ? [index] : [])); + const sorted = slots + .map((index) => all[index]!) + .sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0)); + slots.forEach((slot, index) => (all[slot] = sorted[index]!)); + return all; + } + private settleGenericDemandPriorities(): void { const functionQueue: GenericInstance[] = []; const classQueue: ClassInfo[] = []; @@ -3187,6 +3255,60 @@ export class Lowerer { constructedClassCount(): number { return this.constructedClasses?.size ?? -1; } + /** Helpers that every site asking for them shares (shareHelper). */ + private readonly sharedHelpers = new Set(); + private readonly sharedHelperNames = new Set(); + /** Marks a helper, or the lifted helpers of a name, as shared by every + * site that asks for it. A shared helper is built for whichever site asks + * first, so once lowering finishes its locations are replaced by one that + * names no site. */ + shareHelper(helper: IrFunction | string): void { + if (typeof helper === "string") this.sharedHelperNames.add(helper); + else this.sharedHelpers.add(helper); + } + /** Gives each shared helper the location of the entry file's start: the + * helpers marked by shareHelper, and those interned by key in the helper + * tables. */ + private settleSharedHelperLocations(): void { + const loc: SrcLoc = { file: this.entry.fileName, start: 0, end: 0 }; + const interned = new Set([...this.sharedHelperNames, ...this.classMethodValueHelpers]); + for (const table of [ + this.equalityHelpers, + this.arrHofHelpers, + this.unionCallHelpers, + this.valueHelpers, + this.assertHelpers, + this.inspectHelpers, + this.iterDrainHelpers, + this.mapHofHelpers, + this.setHofHelpers, + this.spHofHelpers, + this.primitiveCtorFns, + this.builtinCallableValueFns, + this.regionRunHelpers, + this.internTableHelpers, + this.coercions.retags, + this.coercions.narrows, + ]) + for (const name of table.values()) interned.add(name); + for (const fn of [...this.liftedFns, ...this.implicitFns]) + if (interned.has(fn.name)) this.sharedHelpers.add(fn); + const seen = new Set(); + const visit = (node: unknown): void => { + if (typeof node !== "object" || node === null || seen.has(node)) return; + seen.add(node); + if (Array.isArray(node)) { + for (const item of node) visit(item); + return; + } + const record = node as Record; + for (const key of Object.keys(record)) { + if (key === "loc") record[key] = loc; + else if (key !== "type") visit(record[key]); + } + }; + for (const fn of this.sharedHelpers) visit(fn); + } /** The first lifted function named `name`, the one * `liftedFns.find((fn) => fn.name === name)` returns. */ liftedFnNamed(name: string | undefined): IrFunction | undefined { @@ -7279,6 +7401,7 @@ export class Lowerer { // Lambdas lifted while lowering any of the above (plus synthetic // array-HOF loop functions, which ride the same list), and the // implicit-any instances lowered eagerly at their first call sites. + this.settleSharedHelperLocations(); functions.push(...this.liftedFns); functions.push(...this.implicitFns); functions.push(...runtimeClassConstructs(this, functions)); @@ -7412,7 +7535,7 @@ export class Lowerer { classes: artifacts.classes, records: artifacts.records, unions: artifacts.unions, - globals: this.globalsList, + globals: this.globalsInTableOrder(), entry: ENTRY_NAME, ...(this.usesWorkers ? { workers: true } : {}), ...(this.usesLifetimeRegions ? { lifetimeRegions: true as const } : {}), @@ -7859,6 +7982,15 @@ export class Lowerer { // exactly like generic instances. while (specLowered < this.emitSpecQueue.length) { const waveEnd = this.emitSpecQueue.length; + // A wave's specializations lower by class and event, not in the + // order bodies asked for them. + const wave = this.emitSpecQueue.slice(specLowered, waveEnd); + const specKey = ({ info, event }: EmitSpecRequest): string => + `${info.def.name}\0${event}`; + wave.sort((left, right) => + specKey(left) < specKey(right) ? -1 : specKey(left) > specKey(right) ? 1 : 0, + ); + this.emitSpecQueue.splice(specLowered, wave.length, ...wave); this.checker.prefetchRoots( this.emitSpecQueue .slice(specLowered, waveEnd) @@ -7908,9 +8040,11 @@ export class Lowerer { // seed the worklist beside the init bodies. Unknown names are inert // (the export-map resolution reports them as SC4002 later). for (const root of extraRoots ?? []) this.onEdge?.(root); + const shuffle = lowerShuffle(); const drainUnits = (): void => { while (queue.length > 0) { const wave = queue.splice(0); + shuffle?.(wave); this.checker.prefetchRoots(wave.flatMap((name) => units.get(name)!.roots)); for (const name of wave) { // A body-level poison outside the per-statement catches (a fenced @@ -7940,21 +8074,28 @@ export class Lowerer { // queue above is the only portion whose discovery order differed from // historical emit order. const classDispatch = new ClassDynamicDispatch(extraRoots !== undefined); + // Units in declaration order, so the passes below meet functions, and the + // classes they collect, in an order that does not depend on lowering order. + const unitsInOrder = (): IrFunction[] => + [...loweredUnits] + .sort(([left], [right]) => units.get(left)!.order - units.get(right)!.order) + .map(([, fn]) => fn); for (;;) { drainInstances(); + const declared = unitsInOrder(); const initializerChanged = this.deferredModuleInitializers.process(this, [ - ...loweredUnits.values(), + ...declared, ...initFunctions, ...instanceFunctions, - ...this.liftedFns, - ...this.implicitFns, + ...byName(this.liftedFns), + ...byName(this.implicitFns), ]); const dispatchChanged = classDispatch.process(this, [ - ...loweredUnits.values(), + ...declared, ...initFunctions, ...instanceFunctions, - ...this.liftedFns, - ...this.implicitFns, + ...byName(this.liftedFns), + ...byName(this.implicitFns), ]); if (queue.length === 0 && !dispatchChanged && !initializerChanged) break; drainUnits(); @@ -7962,9 +8103,7 @@ export class Lowerer { this.restoreGenericClassInstanceOrder(clsInstLowered); } classDispatch.finalize(this); - const orderedUnits = [...loweredUnits] - .sort(([left], [right]) => units.get(left)!.order - units.get(right)!.order) - .map(([, fn]) => fn); + const orderedUnits = unitsInOrder(); this.settleGenericDemandPriorities(); this.shapes.settleDeclaredOrderPriorities(); for (const finalize of this.shapeOrderMetadataFinalizers) finalize(); @@ -7977,13 +8116,16 @@ export class Lowerer { ]); observeRecordLiteralRebuilds(null); for (const finalize of this.shapeOrderHelperFinalizers) finalize(); + this.settleSharedHelperLocations(); + // Lifted and implicit functions are created in lowering order; they + // are assembled by name. const functions = [ ...orderedUnits, ...initFunctions, this.buildMain(), ...instanceFunctions, - ...this.liftedFns, - ...this.implicitFns, + ...byName(this.liftedFns), + ...byName(this.implicitFns), ]; functions.push(...runtimeClassConstructs(this, functions)); if (this.regexGroupResults) for (const fn of functions) foldRegexSpanReads(this, fn); diff --git a/packages/compiler/test/format-float-arguments.test.ts b/packages/compiler/test/format-float-arguments.test.ts index 8aa08469da..e59e023ca7 100644 --- a/packages/compiler/test/format-float-arguments.test.ts +++ b/packages/compiler/test/format-float-arguments.test.ts @@ -27,8 +27,9 @@ test("util.format %f lowers a boolean argument once", async () => { const helpers = mod.functions .map((f) => f.name) .filter((name) => name.startsWith("%arr.")) - .map((name) => name.replace(/\.\d+$/, "")); - expect(helpers).toEqual(["%arr.some", "%arr.every"]); + .map((name) => name.replace(/\.\d+$/, "")) + .sort(); + expect(helpers).toEqual(["%arr.every", "%arr.some"]); } finally { await rm(dir, { recursive: true, force: true }); } diff --git a/scripts/lowering-order.mjs b/scripts/lowering-order.mjs new file mode 100644 index 0000000000..ecfc20955d --- /dev/null +++ b/scripts/lowering-order.mjs @@ -0,0 +1,564 @@ +#!/usr/bin/env node +/* Finds lowering state whose result depends on the order reachable bodies + * lower in. + * + * Each program is lowered to IR in discovery order and with every seed of + * SCRIPTC_LOWER_SHUFFLE_SEED (lowerer.ts), and the IR is compared function + * by function. Output assembly does not depend on the order, so every + * difference is state one body's lowering leaves for another. The + * discovery-order run also audits the lowerer's Map and Set fields: a key + * that two bodies touch, at least one of them writing it, makes the field + * order-sensitive for both bodies. A function whose IR differs between + * orders is attributed to the order-sensitive fields of the body that + * produced it. + * + * node scripts/lowering-order.mjs [--stride 7] [--offset 0] [--limit N] + * [--seeds 1,2,3] [--jobs 8] [--entry ]... [--out ] + * [--fail-on names|content] + * + * Needs a built compiler (pnpm build). Writes /results.json and prints + * a ranking of fields by the programs whose IR they change. With --fail-on, + * it exits with status 1 when some program's IR differs between orders: + * in any way (names), or beyond names and ids (content). */ +import { spawn } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { cpus, tmpdir } from "node:os"; +import { basename, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { parseArgs } from "node:util"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const dist = join(root, "packages/compiler/dist"); + +if (process.argv[2] === "--child") { + const [, , , entry, irPath, seed, auditPath, snapshotPath] = process.argv; + if (seed !== "-") process.env["SCRIPTC_LOWER_SHUFFLE_SEED"] = seed; + const module = (path) => import(pathToFileURL(join(dist, path)).href); + const audit = + auditPath === "-" + ? null + : installAudit( + await module("frontend/lowering/lowerer.js"), + await module("frontend/lowering/class-dynamic-dispatch.js"), + await module("frontend/lowering/deferred-module-initializers.js"), + ); + const snapshots = installSnapshot(await module("frontend/lowering/lowerer.js")); + const { compile } = await import(pathToFileURL(join(dist, "index.js")).href); + const outDir = join(tmpdir(), `scriptc-lowering-order-${process.pid}`); + const result = await compile(entry, { + outDir, + outPath: irPath, + backend: "llvm", + outputKind: "ir", + }); + if (audit !== null) writeFileSync(auditPath, JSON.stringify(audit.report())); + writeFileSync(snapshotPath, JSON.stringify(snapshots.at(-1) ?? {})); + process.exit(result.ok ? 0 : 3); +} + +/** Records the contents of the lowerer's Map and Set fields once reachable + * emit finishes, under keys that do not depend on the process: syntax + * nodes by file and span, symbols by name and declaration, IR values by id + * or name. Values are digested to a bounded depth. */ +function installSnapshot({ Lowerer }) { + const snapshots = []; + const nodeKey = (node) => { + try { + return `node:${node.getSourceFile().fileName}:${node.kind}:${node.pos}:${node.end}`; + } catch { + return `node:?:${node.kind}:${node.pos}:${node.end}`; + } + }; + const isNode = (value) => + typeof value.kind === "number" && + typeof value.pos === "number" && + typeof value.end === "number" && + typeof value.getSourceFile === "function"; + const isSymbol = (value) => + typeof value.escapedName === "string" && Array.isArray(value.declarationHandles); + const symbolKey = (symbol) => { + let declaration = ""; + try { + const first = symbol.declarations[0]; + if (first !== undefined) declaration = nodeKey(first); + } catch {} + return `symbol:${symbol.escapedName}:${declaration}`; + }; + const key = (value) => { + if (value === null || (typeof value !== "object" && typeof value !== "function")) + return `${typeof value}:${String(value)}`; + if (isNode(value)) return nodeKey(value); + if (isSymbol(value)) return symbolKey(value); + for (const field of ["name", "id", "localId"]) + if (typeof value[field] === "string" || typeof value[field] === "number") + return `${value.constructor?.name}:${field}:${value[field]}`; + if (value.def && typeof value.def.name === "string") + return `${value.constructor?.name}:def:${value.def.name}`; + return null; + }; + const digest = (value, depth) => { + if (value === null || value === undefined || typeof value !== "object") + return typeof value === "function" ? "fn" : (JSON.stringify(value) ?? "undefined"); + const stable = key(value); + if (depth === 0) return stable ?? `<${value.constructor?.name}>`; + if (isNode(value) || isSymbol(value)) return stable; + if (Array.isArray(value)) return `[${value.map((item) => digest(item, depth - 1)).join(",")}]`; + if (value instanceof Map) + return `Map{${[...value] + .map(([k, v]) => `${key(k) ?? "?"}=>${digest(v, depth - 1)}`) + .sort() + .join(",")}}`; + if (value instanceof Set) + return `Set{${[...value] + .map((item) => key(item) ?? digest(item, depth - 1)) + .sort() + .join(",")}}`; + const fields = Object.keys(value).sort(); + return `${value.constructor?.name ?? "Object"}{${fields.map((field) => `${field}:${digest(value[field], depth - 1)}`).join(",")}}`; + }; + const emitReachable = Lowerer.prototype.emitReachable; + Lowerer.prototype.emitReachable = function (...args) { + const result = emitReachable.apply(this, args); + const snapshot = {}; + for (const [field, value] of Object.entries(this)) { + if (!(value instanceof Map) && !(value instanceof Set)) continue; + const entries = {}; + const unkeyed = []; + for (const item of value instanceof Map ? value : [...value].map((v) => [v, true])) { + const [k, v] = item; + const stable = key(k); + const text = digest(v, 3); + if (stable === null) unkeyed.push(text); + else entries[stable] = text; + } + snapshot[field] = { entries, unkeyed: unkeyed.sort() }; + } + snapshots.push(snapshot); + return result; + }; + return snapshots; +} + +/** Wraps the lowering methods that produce bodies and the lowerer's Map and + * Set fields; records which bodies touch which keys. */ +function installAudit({ Lowerer }, { ClassDynamicDispatch }, { DeferredModuleInitializers }) { + const proto = Lowerer.prototype; + const tokens = []; + const stack = []; + const fields = new Map(); + const objectIds = new Map(); + const keyId = (key) => { + if (key === null || (typeof key !== "object" && typeof key !== "function")) + return `${typeof key}:${String(key)}`; + let id = objectIds.get(key); + if (id === undefined) objectIds.set(key, (id = objectIds.size)); + return `object:${id}`; + }; + const current = () => (stack.length === 0 ? null : stack[stack.length - 1]); + const touch = (field, key, write) => { + const token = current(); + if (token === null) return; + let keys = fields.get(field); + if (keys === undefined) fields.set(field, (keys = new Map())); + const id = key === ITERATE ? "*" : keyId(key); + let entry = keys.get(id); + if (entry === undefined) keys.set(id, (entry = { readers: new Set(), writers: new Set() })); + (write ? entry.writers : entry.readers).add(token.id); + }; + const ITERATE = Symbol("iterate"); + const instrument = (field, value) => { + const read = (key) => touch(field, key, false); + const write = (key) => { + touch(field, key, true); + // A write changes what every iteration of the field observes. + touch(field, ITERATE, true); + }; + const iterate = () => touch(field, ITERATE, false); + const base = value instanceof Map ? Map.prototype : Set.prototype; + for (const name of ["get", "has"]) { + if (!(name in base)) continue; + const original = base[name]; + value[name] = function (key) { + read(key); + return original.call(this, key); + }; + } + for (const name of ["set", "add", "delete"]) { + if (!(name in base)) continue; + const original = base[name]; + value[name] = function (key, ...rest) { + write(key); + return original.call(this, key, ...rest); + }; + } + for (const name of ["forEach", "keys", "values", "entries", Symbol.iterator]) { + const original = base[name]; + value[name] = function (...args) { + iterate(); + return original.apply(this, args); + }; + } + const size = Object.getOwnPropertyDescriptor(base, "size").get; + Object.defineProperty(value, "size", { + get() { + iterate(); + return size.call(this); + }, + }); + }; + const emitReachable = proto.emitReachable; + proto.emitReachable = function (...args) { + for (const [field, value] of Object.entries(this)) + if (value instanceof Map || value instanceof Set) instrument(field, value); + return emitReachable.apply(this, args); + }; + const produced = (result, into) => { + if (result === null || result === undefined) return; + if (Array.isArray(result)) for (const fn of result) produced(fn, into); + else if (typeof result === "object" && typeof result.name === "string") into.add(result.name); + }; + // Passes over the lowered bodies after the worklist: the functions they + // create belong to the pass. + const passes = new Map(); + const wrapPass = (owner, label, method) => { + const original = owner.prototype[method]; + owner.prototype[method] = function (lowerer, ...rest) { + const implicit = lowerer.implicitFns?.length ?? 0; + const lifted = lowerer.liftedFns?.length ?? 0; + try { + return original.call(this, lowerer, ...rest); + } finally { + let names = passes.get(label); + if (names === undefined) passes.set(label, (names = new Set())); + for (const fn of (lowerer.implicitFns ?? []).slice(implicit)) names.add(fn.name); + for (const fn of (lowerer.liftedFns ?? []).slice(lifted)) names.add(fn.name); + } + }; + }; + wrapPass(ClassDynamicDispatch, "ClassDynamicDispatch.process", "process"); + wrapPass(ClassDynamicDispatch, "ClassDynamicDispatch.finalize", "finalize"); + wrapPass(DeferredModuleInitializers, "DeferredModuleInitializers.process", "process"); + for (const name of [ + "lowerFunction", + "lowerClassCtor", + "lowerClassMethodMember", + "lowerClassMembers", + "lowerFileInit", + "lowerGenericInstance", + "throwingSetterFn", + ]) { + const original = proto[name]; + if (typeof original !== "function") continue; + proto[name] = function (...args) { + const token = { id: tokens.length, names: new Set() }; + tokens.push(token); + const implicit = this.implicitFns?.length ?? 0; + const lifted = this.liftedFns?.length ?? 0; + stack.push(token); + try { + const result = original.apply(this, args); + produced(result, token.names); + return result; + } finally { + stack.pop(); + for (const fn of (this.implicitFns ?? []).slice(implicit)) token.names.add(fn.name); + for (const fn of (this.liftedFns ?? []).slice(lifted)) token.names.add(fn.name); + } + }; + } + return { + report() { + const out = {}; + for (const [field, keys] of fields) { + const involved = new Set(); + let sensitive = 0; + for (const { readers, writers } of keys.values()) { + const all = new Set([...readers, ...writers]); + if (writers.size === 0 || all.size < 2) continue; + sensitive++; + for (const id of all) for (const fn of tokens[id].names) involved.add(fn); + } + if (sensitive > 0) out[field] = { sensitiveKeys: sensitive, functions: [...involved] }; + } + for (const [label, names] of passes) + if (names.size > 0) out[`pass ${label}`] = { sensitiveKeys: 0, functions: [...names] }; + return out; + }, + }; +} + +const { values } = parseArgs({ + options: { + stride: { type: "string", default: "7" }, + offset: { type: "string", default: "0" }, + limit: { type: "string", default: "100000" }, + seeds: { type: "string", default: "1,2,3" }, + jobs: { type: "string", default: String(Math.max(1, Math.floor(cpus().length / 2))) }, + entry: { type: "string", multiple: true, default: [] }, + out: { type: "string", default: join(tmpdir(), "scriptc-lowering-order") }, + "no-audit": { type: "boolean", default: false }, + "fail-on": { type: "string" }, + }, +}); +if (values["fail-on"] !== undefined && !["names", "content"].includes(values["fail-on"])) + throw new Error(`--fail-on takes names or content, not ${values["fail-on"]}`); +const out = resolve(values.out); +mkdirSync(out, { recursive: true }); +const seeds = values.seeds.split(",").filter(Boolean); +const corpus = join(root, "tests/corpus"); +const programs = [ + ...readdirSync(corpus) + .filter((name) => /^\d+-.*\.ts$/.test(name)) + .filter((name) => Number.parseInt(name, 10) % Number(values.stride) === Number(values.offset)) + .sort((a, b) => Number.parseInt(a, 10) - Number.parseInt(b, 10)) + .slice(0, Number(values.limit)) + .map((name) => join(corpus, name)), + ...values.entry.map((entry) => resolve(entry)), +]; + +const child = (entry, irPath, seed, auditPath, snapshotPath) => + new Promise((done) => { + const proc = spawn( + process.execPath, + [ + "--max-old-space-size=8192", + fileURLToPath(import.meta.url), + "--child", + entry, + irPath, + seed, + auditPath, + snapshotPath, + ], + { stdio: ["ignore", "ignore", "pipe"], env: { ...process.env, SCRIPTC_NO_CACHE: "1" } }, + ); + let stderr = ""; + proc.stderr.on("data", (chunk) => (stderr += chunk)); + proc.on("close", (code) => done({ ok: code === 0, stderr: stderr.slice(-2000) })); + }); + +const loadIr = (path) => JSON.parse(readFileSync(path, "utf8")); +const firstDiff = (x, y, path = "") => { + if (JSON.stringify(x) === JSON.stringify(y)) return null; + if (typeof x !== "object" || typeof y !== "object" || x === null || y === null) return path; + for (const key of new Set([...Object.keys(x), ...Object.keys(y)])) { + const found = firstDiff(x[key], y[key], `${path}/${key}`); + if (found !== null) return found; + } + return path; +}; +const differingFunctions = (a, b) => { + const left = new Map(a.functions.map((fn) => [fn.name, fn])); + const right = new Map(b.functions.map((fn) => [fn.name, fn])); + const names = new Set(); + const paths = {}; + for (const [name, fn] of left) { + const other = right.get(name); + if (other === undefined) names.add(name); + else if (JSON.stringify(other) !== JSON.stringify(fn)) { + names.add(name); + paths[name] = firstDiff(fn, other).replace(/\/\d+/g, "/#"); + } + } + for (const name of right.keys()) if (!left.has(name)) names.add(name); + const topLevel = []; + for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) + if (key !== "functions" && JSON.stringify(a[key]) !== JSON.stringify(b[key])) + topLevel.push(key); + if (a.functions.map((fn) => fn.name).join("\n") !== b.functions.map((fn) => fn.name).join("\n")) + topLevel.push("function order"); + return { functions: [...names], topLevel, paths }; +}; + +/** The module with every numbered function, record and union renamed by a + * hash of its own definition (digits stripped), other numbered strings by + * first use within their function or table entry, source locations + * dropped, and tables sorted. Orders whose canonical modules match differ + * only in names and ids numbered in creation order. */ +const canonicalModule = (mod) => { + const strip = (v) => JSON.stringify(v).replace(/\d+/g, "#"); + const hash = (text) => createHash("sha256").update(text).digest("hex").slice(0, 12); + const m = JSON.parse( + JSON.stringify(mod, (k, v) => (k === "loc" || k === "source" ? undefined : v)), + ); + const rename = new Map(); + for (const fn of m.functions ?? []) { + if (!/\d/.test(fn.name)) continue; + const { name, ...rest } = fn; + rename.set(name, `${name.replace(/\d+/g, "#")}~${hash(strip(rest))}`); + } + for (const key of ["records", "unions"]) + for (const item of m[key] ?? []) { + const { id, ...rest } = item; + if (typeof id === "string" && /\d/.test(id)) + rename.set(id, `${id.replace(/\d+/g, "#")}~${hash(strip(rest))}`); + } + const walk = (v, scope) => { + if (typeof v === "string") { + const renamed = rename.get(v); + if (renamed !== undefined) return renamed; + if (!/\d/.test(v)) return v; + let id = scope.get(v); + if (id === undefined) scope.set(v, (id = scope.size)); + return `${v.replace(/\d+/g, "#")}@${id}`; + } + if (Array.isArray(v)) return v.map((x) => walk(x, scope)); + if (v && typeof v === "object") + return Object.fromEntries(Object.keys(v).map((k) => [k, walk(v[k], scope)])); + return v; + }; + const out = {}; + for (const key of Object.keys(m)) + out[key] = Array.isArray(m[key]) + ? m[key].map((item) => JSON.stringify(walk(item, new Map()))).sort() + : JSON.stringify(walk(m[key], new Map())); + return out; +}; +/** Canonical entries present in one order only, by table. */ +const contentDifferences = (a, b) => { + const found = {}; + for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) { + if (JSON.stringify(a[key]) === JSON.stringify(b[key])) continue; + if (!Array.isArray(a[key]) || !Array.isArray(b[key])) { + found[key] = ["(differs)"]; + continue; + } + const other = new Set(b[key]); + found[key] = a[key] + .filter((item) => !other.has(item)) + .map((item) => JSON.parse(item).name ?? JSON.parse(item).id ?? "?"); + } + return found; +}; + +/** Fields whose contents after reachable emit differ between two orders. */ +const differingState = (a, b) => { + const fields = []; + for (const field of new Set([...Object.keys(a), ...Object.keys(b)])) { + const x = a[field] ?? { entries: {}, unkeyed: [] }; + const y = b[field] ?? { entries: {}, unkeyed: [] }; + const keys = new Set([...Object.keys(x.entries), ...Object.keys(y.entries)]); + let differs = [...keys].some((k) => x.entries[k] !== y.entries[k]); + if (!differs) differs = JSON.stringify(x.unkeyed) !== JSON.stringify(y.unkeyed); + if (differs) fields.push(field); + } + return fields; +}; + +const results = []; +let next = 0; +await Promise.all( + Array.from({ length: Number(values.jobs) }, async () => { + while (next < programs.length) { + const entry = programs[next++]; + const name = basename(entry).replace(/\.ts$/, ""); + const dir = join(out, name); + mkdirSync(dir, { recursive: true }); + const audit = values["no-audit"] ? "-" : join(dir, "audit.json"); + const base = await child( + entry, + join(dir, "base.ir.json"), + "-", + audit, + join(dir, "base.state.json"), + ); + if (!base.ok) { + results.push({ program: name, status: "does not compile" }); + continue; + } + const diffs = {}; + const stateDiffs = {}; + let baseCanonical = null; + for (const seed of seeds) { + const run = await child( + entry, + join(dir, `seed${seed}.ir.json`), + seed, + "-", + join(dir, `seed${seed}.state.json`), + ); + if (!run.ok) { + diffs[seed] = { failed: run.stderr }; + continue; + } + const diff = differingFunctions( + loadIr(join(dir, "base.ir.json")), + loadIr(join(dir, `seed${seed}.ir.json`)), + ); + const state = differingState( + loadIr(join(dir, "base.state.json")), + loadIr(join(dir, `seed${seed}.state.json`)), + ); + if (state.length > 0) stateDiffs[seed] = state; + if (diff.functions.length > 0 || diff.topLevel.length > 0) { + baseCanonical ??= canonicalModule(loadIr(join(dir, "base.ir.json"))); + diff.content = contentDifferences( + baseCanonical, + canonicalModule(loadIr(join(dir, `seed${seed}.ir.json`))), + ); + diffs[seed] = diff; + } + } + const auditReport = audit === "-" ? {} : JSON.parse(readFileSync(audit, "utf8")); + const changed = new Set(Object.values(diffs).flatMap((diff) => diff.functions ?? [])); + const attribution = {}; + for (const fn of changed) { + const fields = Object.entries(auditReport) + .filter(([, entry]) => entry.functions.includes(fn)) + .map(([field]) => field); + attribution[fn] = fields; + } + results.push({ + program: name, + status: + Object.keys(diffs).length === 0 + ? "order-independent" + : Object.values(diffs).some((diff) => Object.keys(diff.content ?? {}).length > 0) + ? "order-dependent content" + : "order-dependent names", + diffs, + attribution, + sensitiveFields: Object.keys(auditReport), + stateFields: [...new Set(Object.values(stateDiffs).flat())], + }); + process.stderr.write( + `${results.length}/${programs.length} ${name}: ${results.at(-1).status}\n`, + ); + } + }), +); +writeFileSync(join(out, "results.json"), JSON.stringify(results, null, 2)); + +const dependent = results.filter((r) => r.status.startsWith("order-dependent")); +const content = results.filter((r) => r.status === "order-dependent content"); +const byField = new Map(); +let unattributed = 0; +for (const r of dependent) { + const fields = new Set(Object.values(r.attribution).flat()); + if (fields.size === 0) unattributed++; + for (const field of fields) byField.set(field, (byField.get(field) ?? 0) + 1); +} +console.log( + `programs ${results.length}: ${dependent.length} order-dependent (${content.length} beyond names and ids), ${results.filter((r) => r.status === "does not compile").length} do not compile`, +); +for (const r of content) + console.log( + ` ${r.program}: ${Object.values(r.diffs) + .flatMap((diff) => + Object.entries(diff.content ?? {}).map( + ([table, items]) => `${table}: ${items.slice(0, 4).join(", ")}`, + ), + ) + .slice(0, 3) + .join("; ")}`, + ); +for (const [field, count] of [...byField].sort((a, b) => b[1] - a[1])) + console.log(`${String(count).padStart(5)} ${field}`); +console.log(`${String(unattributed).padStart(5)} (no audited field)`); +const failing = + values["fail-on"] === "names" ? dependent : values["fail-on"] === "content" ? content : []; +if (failing.length > 0) { + console.log(`${failing.length} programs lower to different IR in another order`); + process.exitCode = 1; +} From 3b7f33c22393afe56700a48feebc4a091a975840 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 20:35:09 -0500 Subject: [PATCH 032/395] Leave SC6101 out of the native compiler's self-builds The compiler's own source has 24 likely-hot functions that use untyped values, so its self-builds now print SC6101. The native bootstrap contracts compare stderr exactly; the rebuilds of the compiler entry pass --no-dynamic-tier-warning, which changes no IR or LLVM output. --- .../self-hosting-native-driver.test.ts | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/harness/self-hosting-native-driver.test.ts b/tests/harness/self-hosting-native-driver.test.ts index 722c3fede5..1cc920bd2e 100644 --- a/tests/harness/self-hosting-native-driver.test.ts +++ b/tests/harness/self-hosting-native-driver.test.ts @@ -185,6 +185,10 @@ test( return result.stdout; }; const entry = join(root, "packages/compiler/src/native/cli.ts"); + // The compiler's own source has untyped functions that SC6101 reports + // (diagnostics/dynamic-tier.ts). The contracts compare stderr exactly, + // so its self-builds leave that performance note out. + const quiet = "--no-dynamic-tier-warning"; const ffi = join(directory, ".scriptc/distribution-seed/compiler.ffi.json"); const rebuilt = executable("scriptc-rebuilt"); const checkProgram = async (compiler: string, source: string, extra: string[] = []) => { @@ -541,7 +545,18 @@ test( const self = await bootstrapStep("production CLI rebuilds itself", () => invoke( seed, - ["build", entry, "-o", rebuilt, "--strip", "--keep-llvm", "--emit-ir", "--ffi", ffi], + [ + "build", + entry, + "-o", + rebuilt, + "--strip", + "--keep-llvm", + "--emit-ir", + "--ffi", + ffi, + quiet, + ], "scriptc: warning: --emit-ir is deprecated; use --emit=ir for IR as the primary output\n", ), ); @@ -554,7 +569,16 @@ test( const seedLlvm = join(directory, "cli.ll"); const rebuiltLlvm = join(directory, "rebuilt.ll"); await bootstrapStep("rebuilt compiler emits itself", () => - invoke(rebuilt, ["build", entry, "--emit=llvm", "-o", rebuiltLlvm, "--ffi", ffi]), + invoke(rebuilt, [ + "build", + entry, + "--emit=llvm", + "-o", + rebuiltLlvm, + "--ffi", + ffi, + quiet, + ]), ); // Executable output adds a runtime ABI check; textual LLVM emission // omits it. Compare all other output without changing either mode. @@ -585,7 +609,7 @@ test( const rebuildFrontend = async () => { const lowered = join(directory, "cli.ir.json"); const output = await bootstrapStep("sanitized native frontend lowers the compiler", () => - invoke(seed, ["build", entry, "--emit=ir", "-o", lowered, "--ffi", ffi]), + invoke(seed, ["build", entry, "--emit=ir", "-o", lowered, "--ffi", ffi, quiet]), ); expect(output.trim()).toBe(lowered); await compareArtifacts({ ir: true, llvm: false }); @@ -605,6 +629,7 @@ test( ffi, "--sanitize", "--optimization=dev", + quiet, ], "", { SCRIPTC_BOOTSTRAP_IR_INPUT: seedIr }, From 462f93d50802826e771341ec81d101b43c45bc1d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 20:42:16 -0500 Subject: [PATCH 033/395] Type the enum entry of a package's declaration exports explicitly The compiler compiles itself natively, and an empty array in an object literal took number[] there, so the enum entry did not match the declaration record. The coverage snapshot of native-npm-imports now counts one dynamic-tier statement instead of three: the program's `const point: Vector` takes the interface from the package's .d.ts instead of any. --- packages/compiler/src/frontend/npm-static-type-exports.ts | 2 +- tests/harness/__snapshots__/coverage-npm-packages.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/npm-static-type-exports.ts b/packages/compiler/src/frontend/npm-static-type-exports.ts index 844afa5900..bdea8eb033 100644 --- a/packages/compiler/src/frontend/npm-static-type-exports.ts +++ b/packages/compiler/src/frontend/npm-static-type-exports.ts @@ -80,7 +80,7 @@ export function npmStaticDeclarationExports(sf: ts.SourceFile): NpmStaticDeclara localTypes.set(decl.name, decl); if (exported) types.push(decl); } else if (ts.isEnumDeclaration(stmt)) { - const decl = { name: stmt.name.text, params: [], templates: [] }; + const decl: NpmStaticTypeDeclaration = { name: stmt.name.text, params: [], templates: [] }; localTypes.set(decl.name, decl); if (exported) types.push(decl); } else if ( diff --git a/tests/harness/__snapshots__/coverage-npm-packages.txt b/tests/harness/__snapshots__/coverage-npm-packages.txt index 475b10e453..6150b08c2a 100644 --- a/tests/harness/__snapshots__/coverage-npm-packages.txt +++ b/tests/harness/__snapshots__/coverage-npm-packages.txt @@ -2,7 +2,7 @@ scriptc coverage tests/corpus/native-npm-imports/main.ts statements analyzed 14 compile statically 14 (100%) - in dynamic tier 3 (operate on values typed only at runtime; --explain-dynamic lists them) + in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them) npm package compilation: mathkit static From 388523cee4936f1c1c1243d87c5174ccd14dce43 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 21:09:39 -0500 Subject: [PATCH 034/395] Address review feedback on the lowering-order check --- scripts/lowering-order.mjs | 83 +++++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/scripts/lowering-order.mjs b/scripts/lowering-order.mjs index ecfc20955d..81611e6a41 100644 --- a/scripts/lowering-order.mjs +++ b/scripts/lowering-order.mjs @@ -371,17 +371,55 @@ const differingFunctions = (a, b) => { return { functions: [...names], topLevel, paths }; }; +/** Keys whose strings name functions, locals, globals, shapes, unions, + * classes and labels. Lowering numbers these in creation order; every other + * string (literal values, field names, library functions) is compared as + * is. A `name` names a function, record or union when it is one of the + * module's numbered definitions or starts with `%`. */ +const ID_KEYS = new Set([ + "base", + "callee", + "captures", + "catchLocalId", + "className", + "fnName", + "fromId", + "globalId", + "id", + "int32Locals", + "int32Params", + "label", + "labels", + "localId", + "retag", + "shapeId", + "targets", + "unionId", +]); + /** The module with every numbered function, record and union renamed by a - * hash of its own definition (digits stripped), other numbered strings by - * first use within their function or table entry, source locations - * dropped, and tables sorted. Orders whose canonical modules match differ - * only in names and ids numbered in creation order. */ + * hash of its own definition (ids with their digits stripped), other + * numbered ids by first use within their function or table entry, source + * locations dropped, and tables sorted. Orders whose canonical modules + * match differ only in names and ids numbered in creation order. */ const canonicalModule = (mod) => { - const strip = (v) => JSON.stringify(v).replace(/\d+/g, "#"); const hash = (text) => createHash("sha256").update(text).digest("hex").slice(0, 12); - const m = JSON.parse( - JSON.stringify(mod, (k, v) => (k === "loc" || k === "source" ? undefined : v)), - ); + const m = JSON.parse(JSON.stringify(mod, (k, v) => (k === "loc" ? undefined : v))); + const definitions = new Set([ + ...(m.functions ?? []).map((fn) => fn.name), + ...["records", "unions"].flatMap((key) => (m[key] ?? []).map((item) => item.id)), + ]); + const isId = (key, value) => + ID_KEYS.has(key) || (key === "name" && (definitions.has(value) || value.startsWith("%"))); + /** `v` with the strings under id keys passed through `id`. */ + const mapIds = (v, id, key) => { + if (typeof v === "string") return isId(key, v) ? id(v) : v; + if (Array.isArray(v)) return v.map((x) => mapIds(x, id, key)); + if (v && typeof v === "object") + return Object.fromEntries(Object.keys(v).map((k) => [k, mapIds(v[k], id, k)])); + return v; + }; + const strip = (v) => JSON.stringify(mapIds(v, (id) => id.replace(/\d+/g, "#"))); const rename = new Map(); for (const fn of m.functions ?? []) { if (!/\d/.test(fn.name)) continue; @@ -394,25 +432,22 @@ const canonicalModule = (mod) => { if (typeof id === "string" && /\d/.test(id)) rename.set(id, `${id.replace(/\d+/g, "#")}~${hash(strip(rest))}`); } - const walk = (v, scope) => { - if (typeof v === "string") { - const renamed = rename.get(v); + const canonical = (item) => { + const scope = new Map(); + return mapIds(item, (value) => { + const renamed = rename.get(value); if (renamed !== undefined) return renamed; - if (!/\d/.test(v)) return v; - let id = scope.get(v); - if (id === undefined) scope.set(v, (id = scope.size)); - return `${v.replace(/\d+/g, "#")}@${id}`; - } - if (Array.isArray(v)) return v.map((x) => walk(x, scope)); - if (v && typeof v === "object") - return Object.fromEntries(Object.keys(v).map((k) => [k, walk(v[k], scope)])); - return v; + if (!/\d/.test(value)) return value; + let id = scope.get(value); + if (id === undefined) scope.set(value, (id = scope.size)); + return `${value.replace(/\d+/g, "#")}@${id}`; + }); }; const out = {}; for (const key of Object.keys(m)) out[key] = Array.isArray(m[key]) - ? m[key].map((item) => JSON.stringify(walk(item, new Map()))).sort() - : JSON.stringify(walk(m[key], new Map())); + ? m[key].map((item) => JSON.stringify(canonical(item))).sort() + : JSON.stringify(canonical(m[key])); return out; }; /** Canonical entries present in one order only, by table. */ @@ -479,7 +514,9 @@ await Promise.all( join(dir, `seed${seed}.state.json`), ); if (!run.ok) { - diffs[seed] = { failed: run.stderr }; + // A program that lowers in discovery order but not in this one + // differs in content. + diffs[seed] = { failed: run.stderr, content: { lowering: ["fails in this order"] } }; continue; } const diff = differingFunctions( From 4a5ddadd3dd1418da78047d42cbca38df2eb4e9a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 21:28:27 -0500 Subject: [PATCH 035/395] Memoize an empty literal's type separately as a union arm The type mapper maps the type inferred from an empty object literal to the empty record as an arm of an unreduced union and to a checked-dynamic value elsewhere, but both answers shared one memo entry. Once the type had been mapped on its own, a union such as `typeof empty | DataView` reused the dynamic answer for its arm and refused (SC2009). The memo key now includes whether the mapping is for such an arm. --- packages/compiler/src/frontend/type-mapper.ts | 5 ++++- tests/corpus/6909-empty-object-type-values.ts | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 6305a6b63a..c171c5c4bb 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1655,7 +1655,10 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const topLevel = mapTypeDepth === 0; const typeId = (type as { id?: number }).id; const memoizable = typeId !== undefined && (ctx.canMemoizeType?.() ?? true); - const memoKey = memoizable ? `${0}:${typeId}` : null; + // An empty literal's type maps to the empty record as an unreduced union's + // arm (emptyLiteralArm) and to the checked-dynamic value elsewhere: the two + // answers keep separate memo entries. + const memoKey = memoizable ? `${ctx.emptyLiteralArm ? 1 : 0}:${typeId}` : null; if (memoKey !== null) { const memo = ctx.typeMemo?.get(memoKey); if (memo !== undefined) return memo; diff --git a/tests/corpus/6909-empty-object-type-values.ts b/tests/corpus/6909-empty-object-type-values.ts index b539415fde..f845e19a6a 100644 --- a/tests/corpus/6909-empty-object-type-values.ts +++ b/tests/corpus/6909-empty-object-type-values.ts @@ -30,3 +30,11 @@ console.log(typeof choose(1), typeof choose(-1)); const empty = {}; console.log(JSON.stringify(empty), Object.keys(empty).length, empty === empty, String(empty)); + +// An empty literal's type beside a typed arm of an unreduced union, after +// the type was already used on its own. +const viewed = {}; +console.log(JSON.stringify(viewed)); +const views: (typeof viewed | DataView)[] = [viewed, new DataView(new ArrayBuffer(2))]; +for (const view of views) + console.log(view instanceof DataView ? `view ${view.byteLength}` : JSON.stringify(view)); From 487f536a2908ae4bb4c755bd1b57768f502c674c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 21:28:46 -0500 Subject: [PATCH 036/395] Keep int32 fields of classes whose prototypes are named for inspection dyn.nameClassPrototype sets the class name on a class's runtime prototype object and stores no property. The int32 field analysis did not list it as read-only, so every program that gave a class a runtime prototype counted a dynamic store that can name any property, and the number fields of every class reaching dynamic code were stored as doubles. --- packages/compiler/src/ir/int32-slots.test.ts | 4 +- packages/compiler/src/ir/int32-slots.ts | 1 + .../test/int32-dynamic-exposure.test.ts | 84 +++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/int32-dynamic-exposure.test.ts diff --git a/packages/compiler/src/ir/int32-slots.test.ts b/packages/compiler/src/ir/int32-slots.test.ts index 446b3512d7..be933ab385 100644 --- a/packages/compiler/src/ir/int32-slots.test.ts +++ b/packages/compiler/src/ir/int32-slots.test.ts @@ -262,7 +262,8 @@ test("dynamic stores disqualify the field names they can reach", () => { expect([readOnly.isField("C", "a"), readOnly.isField("C", "b")]).toEqual([true, true]); // Reflection that only reads the view (Object.keys/values/entries, for-in, // own-key lists, typeof, toString and inspection) stores nothing, nor does - // creating an object from a prototype or reading a prototype. + // creating an object from a prototype, reading a prototype or naming a + // class prototype for inspection. for (const reader of [ "dyn.objKeys", "dyn.objKeyStrings", @@ -282,6 +283,7 @@ test("dynamic stores disqualify the field names they can reach", () => { "insp.dynS", "dyn.objCreate", "dyn.getPrototype", + "dyn.nameClassPrototype", // Registering or removing a process event listener stores nothing either. "process.onSignal", "process.offSignal", diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 781fa86bd1..6ff148c55c 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -376,6 +376,7 @@ const DYN_READ_ONLY = new Set([ "dyn.collectionIteratorIndex", "dyn.mapSeedEntries", "dyn.mapSeedEntry", + "dyn.nameClassPrototype", "dyn.numberConstructor", "dyn.objCreate", "dyn.objEntries", diff --git a/packages/compiler/test/int32-dynamic-exposure.test.ts b/packages/compiler/test/int32-dynamic-exposure.test.ts new file mode 100644 index 0000000000..09330e057b --- /dev/null +++ b/packages/compiler/test/int32-dynamic-exposure.test.ts @@ -0,0 +1,84 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { analyzeInt32Slots, dynamicFieldStores } from "../src/ir/int32-slots.js"; +import type { IrModule } from "../src/ir/ir.js"; + +// Instances of these classes reach checked-dynamic code (an unknown[] array, +// instanceof, inspection, JSON and string conversion), but the program never +// stores a property through a dynamic value. The helpers the compiler emits +// for those reads (prototype objects, inspection names) store nothing, so the +// classes keep their int32 fields. +const source = ` +import { inspect } from "node:util"; + +class Node { + kind: number; + flags = 0; + constructor(kind: number) { + this.kind = kind; + } +} +class Identifier extends Node { + text = "id"; +} +class Type { + constructor( + public flags: number, + public id: number, + ) {} +} + +const values: unknown[] = [new Node(1), new Identifier(2), new Type(4, 7), "s", 3]; +let total = 0; +for (const value of values) { + if (value instanceof Identifier) total += value.kind | value.flags; + else if (value instanceof Node) total += value.kind; + else if (value instanceof Type) total += value.flags & 6; +} +console.log(total, values, inspect(new Type(1, 2))); +console.log(JSON.stringify(values), String(values[0]), typeof values[2]); +`; + +async function lowered(): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-dynamic-")); + try { + const entry = join(dir, "main.ts"); + await writeFile(entry, source); + const outPath = join(dir, "main.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 deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +test("classes that only reach dynamic reads keep their int32 fields", async () => { + const mod = await lowered(); + // Name the functions whose dynamic helpers count as stores that can name + // any property: such a helper either stores into the value or is missing + // from the analysis's read-only list (DYN_READ_ONLY in int32-slots.ts). + const storing = mod.functions + .filter((fn) => dynamicFieldStores({ ...mod, functions: [fn] }).all) + .map((fn) => fn.name); + expect(storing).toEqual([]); + expect(dynamicFieldStores(mod).names.size).toBe(0); + + const slots = analyzeInt32Slots(mod); + const className = (jsName: string): string => + mod.classes!.find((c) => (c.jsName ?? c.name) === jsName || c.name.endsWith(`.${jsName}`))! + .name; + expect([ + slots.isField(className("Node"), "kind"), + slots.isField(className("Node"), "flags"), + slots.isField(className("Identifier"), "kind"), + slots.isField(className("Type"), "flags"), + slots.isField(className("Type"), "id"), + ]).toEqual([true, true, true, true, true]); + // The program reaches the helper that names class prototypes for inspection. + expect(mod.functions.some((fn) => fn.name.startsWith("%prototype.data."))).toBe(true); +}); From c0014f683d15672058569def9a5c5195a6fad58a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 21:29:59 -0500 Subject: [PATCH 037/395] Convert unions of primitives with unary + --- .../frontend/lowering/expressions/updates.ts | 9 +++- .../src/frontend/lowering/lower-calls.ts | 14 ++++++ .../6860-unary-plus-primitive-unions.ts.json | 6 +++ .../6861-unary-plus-optional-values.js.json | 6 +++ .../6860-unary-plus-primitive-unions.ts | 48 ++++++++++++++++++ .../corpus/6861-unary-plus-optional-values.js | 49 +++++++++++++++++++ .../6860-unary-plus-primitive-unions.ts.json | 1 + .../6861-unary-plus-optional-values.js.json | 1 + 8 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6860-unary-plus-primitive-unions.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6861-unary-plus-optional-values.js.json create mode 100644 tests/corpus/6860-unary-plus-primitive-unions.ts create mode 100644 tests/corpus/6861-unary-plus-optional-values.js create mode 100644 tests/harness/corpus-costs/6860-unary-plus-primitive-unions.ts.json create mode 100644 tests/harness/corpus-costs/6861-unary-plus-optional-values.js.json diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index 7269f6b529..13baca7d20 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -30,6 +30,7 @@ import { } from "../../../ir/ir.js"; import { isJsSourceFile, locOf } from "../../program.js"; import { classToNumberConversion } from "../class-to-primitive.js"; +import { lowerPrimitiveUnionNumber } from "../lower-calls.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../../ir/build.js"; import { fenceNodeModuleMutation } from "../lower-node-module.js"; import { staticFieldWriteTarget } from "../lower-classes.js"; @@ -132,8 +133,12 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio const classNumber = classToNumberConversion(lowerer, raw, expr.operand, "unary plus"); if (classNumber) return classNumber; const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand); - if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "+", [raw.type]); - return operand; + if (operand.type.kind === "f64") return operand; + // `+process.argv[2]`: a `string | undefined` (or other primitive + // union) converts as Number(value) does. + const unionNumber = lowerPrimitiveUnionNumber(lowerer, raw, expr.operand, loc); + if (unionNumber) return unionNumber; + operatorUnsupported(lowerer, expr, "+", [raw.type]); } case ts.SyntaxKind.ExclamationToken: { // `!x` is ToBoolean-then-negate: f64/string operands go through toBool. diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index ac77288380..a5c09796c8 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -3201,6 +3201,20 @@ function lowerStringPrototypeCall( ); } +/** ToNumber of a union of strings, numbers, booleans, null and undefined, as + * `Number(value)` converts it; null for other types. Unary `+` takes it too: + * the two conversions differ only for BigInt, which such unions don't hold. */ +export function lowerPrimitiveUnionNumber( + lowerer: Lowerer, + arg: IrExpr, + node: ts.Expression, + loc: SrcLoc, +): IrExpr | null { + return ( + lowerOptionalStringNumber(lowerer, arg, loc) ?? lowerScalarUnionNumber(lowerer, arg, node, loc) + ); +} + function lowerScalarUnionNumber( lowerer: Lowerer, arg: IrExpr, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6860-unary-plus-primitive-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6860-unary-plus-primitive-unions.ts.json new file mode 100644 index 0000000000..9847a1330a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6860-unary-plus-primitive-unions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6860-unary-plus-primitive-unions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6861-unary-plus-optional-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6861-unary-plus-optional-values.js.json new file mode 100644 index 0000000000..7bf8c20a2e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6861-unary-plus-optional-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6861-unary-plus-optional-values.js" + ], + "diags": [] +} diff --git a/tests/corpus/6860-unary-plus-primitive-unions.ts b/tests/corpus/6860-unary-plus-primitive-unions.ts new file mode 100644 index 0000000000..e112013d96 --- /dev/null +++ b/tests/corpus/6860-unary-plus-primitive-unions.ts @@ -0,0 +1,48 @@ +// Unary + on unions of strings, numbers and booleans converts as Number(value) +// does: strings parse with StringToNumber (whitespace, hex, octal and binary +// prefixes, Infinity), booleans become 1 and 0, and numbers keep their value. +function show(label: string, value: number): void { + console.log(label, Object.is(value, -0) ? "-0" : String(value)); +} + +function plusTextOrNumber(value: string | number): number { + return +value; +} + +function plusTextOrFlag(value: string | boolean): number { + return +value; +} + +function plusNumberOrFlag(value: number | boolean): number { + return +value; +} + +type Scalar = string | number | boolean; + +const texts = ["42", " 0x1F ", "", " ", "1e3", "-0", "abc", "Infinity", "0b101", "0o17", "1_000", "\n7\t"]; +for (const text of texts) show(`text ${JSON.stringify(text)}`, plusTextOrNumber(text)); +for (const n of [9, -0, 0.5, Number.NaN, -Infinity]) show(`number ${String(n)}`, plusTextOrNumber(n)); +for (const value of [true, false, "3", "x"]) show(`text or flag ${String(value)}`, plusTextOrFlag(value)); +for (const value of [true, false, 2.5]) show(`number or flag ${String(value)}`, plusNumberOrFlag(value)); + +const scalars: Scalar[] = ["7", 3.5, true, false, "", -0, " 12 "]; +let total = 0; +for (const value of scalars) { + const n = +value; + total += n; + show(`scalar ${typeof value} ${JSON.stringify(value)}`, n); +} +show("total", total); + +// In expressions: sums, negation and comparisons. +const width: string | number = texts.length > 3 ? "640" : 0; +const ratio: string | number = texts.length > 3 ? 1.5 : "2"; +show("sum", +width + +ratio); +show("negated", -+width); +console.log("compare", +width > 600, +ratio === 1.5); + +type Level = "1" | "2" | 3; +function plusLevel(level: Level): number { + return +level; +} +show("level", plusLevel("2") + plusLevel(3)); diff --git a/tests/corpus/6861-unary-plus-optional-values.js b/tests/corpus/6861-unary-plus-optional-values.js new file mode 100644 index 0000000000..6acf48e102 --- /dev/null +++ b/tests/corpus/6861-unary-plus-optional-values.js @@ -0,0 +1,49 @@ +// Unary + in JavaScript on values that may be undefined or null: command-line +// argument slots, Map lookups, optional parameters and nullable values convert +// as Number(value) does (undefined to NaN, null to 0). +function show(label, value) { + console.log(label, Object.is(value, -0) ? "-0" : String(value)); +} + +// No arguments: the first argument slot is undefined. +const n = +process.argv[2]; +show("argv[2]", n); +show("argv[2] doubled", +process.argv[2] * 2); +show("argv[2] or default", +process.argv[2] || 100); +show("argv[0] is a path", +process.argv[0]); + +const settings = new Map([ + ["width", "640"], + ["ratio", "1.5"], + ["blank", " "], +]); +show("map width", +settings.get("width")); +show("map ratio", +settings.get("ratio")); +show("map blank", +settings.get("blank")); +show("map missing", +settings.get("height")); +show("map sum", +settings.get("width") + +settings.get("ratio")); +show("negated", -+settings.get("width")); + +function plusOptional(value) { + return +value; +} +for (const text of ["42", " 0x1F ", "", "1e3", "-0", "abc", undefined]) { + show(`optional ${String(text)}`, plusOptional(text)); +} + +function plusParameter(value = undefined) { + return +value; +} +show("parameter missing", plusParameter()); +show("parameter given", plusParameter("12.5")); + +const words = ["3", "x"]; +show("past the end", +words[5]); +show("in range", +words[0]); + +const lookup = { a: "8" }; +show("missing key", +lookup.b); +show("present key", +lookup.a); + +const maybeNull = words.length > 1 ? null : "1"; +show("null", +maybeNull); diff --git a/tests/harness/corpus-costs/6860-unary-plus-primitive-unions.ts.json b/tests/harness/corpus-costs/6860-unary-plus-primitive-unions.ts.json new file mode 100644 index 0000000000..8211f07162 --- /dev/null +++ b/tests/harness/corpus-costs/6860-unary-plus-primitive-unions.ts.json @@ -0,0 +1 @@ +{ "plain": 2430, "san": 6860 } diff --git a/tests/harness/corpus-costs/6861-unary-plus-optional-values.js.json b/tests/harness/corpus-costs/6861-unary-plus-optional-values.js.json new file mode 100644 index 0000000000..376953c70c --- /dev/null +++ b/tests/harness/corpus-costs/6861-unary-plus-optional-values.js.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 5370 } From 71e118be7be32bd09c7d3a935155b3b8765e1086 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 21:42:23 -0500 Subject: [PATCH 038/395] Count loops bounded by a string's length in integers A counted for loop needs a stable limit to run its counter in integer storage. Array lengths qualified when the loop provably keeps them, but a string's length did not, so `for (let i = 0; i < s.length; i++)` kept a double counter and evaluated the length, through the UTF-16 mapping of a non-ASCII string, on every iteration. A string's length cannot change, so it is now stable whenever the string's binding is not rebound in the loop or boxed. --- .../compiler/src/ir/integer-loops.test.ts | 39 +++++++++++++++++++ packages/compiler/src/ir/integer-loops.ts | 7 ++++ 2 files changed, 46 insertions(+) diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index 75968b2623..aada3b0535 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -209,6 +209,45 @@ test("a string read at the counter is an integer consumer", () => { expect(matchIntegerCountedForLoop(s, withText)).toBeNull(); }); +test("a string length bounds a counted loop while its binding is unchanged", () => { + const withText = new Map(locals); + withText.set("s", { id: "s", name: "s", type: STRING, mutable: true }); + const text = (): IrExpr => ({ kind: "varRef", localId: "s", type: STRING, loc }); + const length: IrExpr = { + kind: "strIntrinsic", + method: "length", + receiver: text(), + args: [], + type: F64, + loc, + }; + const s = counted(length); + s.body = [ + { + kind: "exprStmt", + expr: { + kind: "strIntrinsic", + method: "charCodeAt", + receiver: text(), + args: [ref("i")], + type: F64, + loc, + }, + loc, + }, + ]; + expect(matchIntegerCountedForLoop(s, withText)?.localId).toBe("i"); + // Rebinding the string in the body changes the length the condition reads. + const rebinding = { + ...s, + body: [...s.body, { kind: "assign", localId: "s", value: text(), loc } as IrStmt], + }; + expect(matchIntegerCountedForLoop(rebinding, withText)).toBeNull(); + const boxed = new Map(withText); + boxed.set("s", { ...withText.get("s")!, boxed: true }); + expect(matchIntegerCountedForLoop(s, boxed)).toBeNull(); +}); + test("a stable array length bounds a counted loop; a resizing body keeps it a condition", () => { const ARR = arrayOf(F64); const array = (localId: string): IrExpr => ({ kind: "varRef", localId, type: ARR, loc }); diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index 3ba1863760..9b46756856 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -83,6 +83,13 @@ function stableLoopValue( stableLoopValue(value.left, locals, body, invariant, counter, stableLength) && stableLoopValue(value.right, locals, body, invariant, counter, stableLength) ); + case "strIntrinsic": + // A string's length is fixed: stable while its binding is. + return ( + value.method === "length" && + value.receiver.kind === "varRef" && + stableLoopValue(value.receiver, locals, body, invariant, counter) + ); case "bytesIntrinsic": case "arrIntrinsic": return ( From 283a504fe02195a7d5337127fe35a40c2c8c8a34 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 21:55:47 -0500 Subject: [PATCH 039/395] Support crypto.createDiffieHellman and getDiffieHellman crypto.createDiffieHellman, crypto.getDiffieHellman and crypto.createDiffieHellmanGroup return native DiffieHellman and DiffieHellmanGroup handles backed by mbedTLS bignums. generateKeys, computeSecret, getPrime, getGenerator, getPublicKey, getPrivateKey, setPublicKey, setPrivateKey and verifyError follow Node 24.22: - the modp1, modp2, modp5 and modp14 through modp18 groups, with the private key widths of Node's OpenSSL 3 path (200 to 400 bits for modp5 and up, [2, p - 2] for modp1 and modp2); - explicit primes and generators in every argument form and encoding, with keys of bits(p) - 2 bits, or below min(2^N, q) when the parameters equal an OpenSSL named group (RFC 3526, RFC 7919, RFC 5114); - generated safe primes with OpenSSL's congruences for the generator; - verifyError's DH_check bits, computed when it is first read; - Node's argument, generator, modulus, key range, state and encoding errors, including the OpenSSL errors' library and reason properties. Safe-prime generation sieves candidates with the odd primes below a bound that grows with the prime's length and tests them with Miller-Rabin rounds over a variable-time Montgomery exponentiation that only reads public values; key and secret exponentiations stay on mbedTLS's constant-time path. Primes longer than 8192 bits, property reads other than verifyError, property writes, Object.prototype methods, and calls whose output encoding is typed any or matches no @types/node overload are refused. The runtime unit scr_dh.c links, with the mbedTLS archive, only into programs that use DiffieHellman. wasm32-wasi builds refuse it. The ECDH unit's argument and encoding helpers move to scr_crypto_args.h, which both units include. --- .../limitations/builtin-apis/crypto.mdx | 6 + .../src/generated/node-v24-compatibility.json | 110 +- .../generated/node-v24-internal.json | 237 +++- internal/compatibility/src/generate.mjs | 16 +- internal/compatibility/static-support.json | 5 + .../ambient/scriptc-node-fallback.d.ts | 62 + .../src/backend/executable-features.ts | 1 + packages/compiler/src/backend/library-pack.ts | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 2 + .../src/backend/native-link-info-core.ts | 3 + .../compiler/src/backend/native/contracts.ts | 4 + .../compiler/src/backend/native/executable.ts | 9 +- .../src/backend/runtime-pack-targets.test.ts | 1 + .../compiler/src/backend/runtime-pack.test.ts | 1 + .../src/backend/target-diagnostics.ts | 7 + packages/compiler/src/backend/warm-cache.ts | 1 + .../src/executable/executable-cache.test.ts | 1 + .../src/executable/executable-cache.ts | 2 + .../src/frontend/lowering/builtins/crypto.ts | 4 + .../src/frontend/lowering/builtins/dh.ts | 166 +++ .../src/frontend/lowering/builtins/ecdh.ts | 19 +- .../compiler/src/frontend/lowering/lowerer.ts | 13 +- .../src/frontend/lowering/surfaces.ts | 13 +- packages/compiler/src/frontend/type-mapper.ts | 17 + packages/compiler/src/index.ts | 1 + packages/compiler/src/ir/builtin-effects.ts | 3 + .../compiler/src/ir/builtin-signatures.ts | 7 + packages/compiler/src/ir/runtime-features.ts | 6 + packages/compiler/src/library/ir-policy.ts | 2 + packages/compiler/src/native/cli.ts | 1 + packages/compiler/src/native/prepare.ts | 1 + .../compiler/src/native/sanitizer.test.ts | 1 + packages/compiler/surface-manifest.json | 14 +- packages/compiler/test/cc-driver.test.ts | 5 + .../tests/corpus/6706-dh-groups.ts.json | 6 + .../corpus/6707-dh-explicit-params.ts.json | 6 + .../tests/corpus/6708-dh-errors.js.json | 6 + .../tests/corpus/6709-dh-generate.ts.json | 6 + .../tests/corpus/6710-dh-encodings.cjs.json | 6 + .../tests/corpus/6711-dh-handles.ts.json | 6 + .../tests/fixtures/node-types/dh.ts.json | 6 + .../runtime-pack-matrix.mjs | 2 +- .../runtime-darwin-arm64/scripts/build.mjs | 3 +- .../runtime-pack-matrix.mjs | 3 +- .../runtime-pack-common/scripts/build.mjs | 3 +- .../runtime-wasm32-wasi/scripts/build.mjs | 1 + packages/runtime/src/scr_crypto.h | 7 + packages/runtime/src/scr_crypto_args.h | 181 +++ packages/runtime/src/scr_dh.c | 1186 +++++++++++++++++ packages/runtime/src/scr_dh_groups.h | 557 ++++++++ packages/runtime/src/scr_dyn.h | 2 + packages/runtime/src/scr_ecdh.c | 192 +-- packages/runtime/test/dh.test.ts | 85 ++ packages/runtime/test/test_dh.c | 228 ++++ tests/corpus/6706-dh-groups.ts | 52 + tests/corpus/6707-dh-explicit-params.ts | 169 +++ tests/corpus/6708-dh-errors.js | 147 ++ tests/corpus/6709-dh-generate.ts | 47 + tests/corpus/6710-dh-encodings.cjs | 73 + tests/corpus/6711-dh-handles.ts | 56 + tests/fixtures/node-types/dh.ts | 22 + tests/fixtures/node-types/fenced.ts | 6 + .../__snapshots__/node-types-fenced.txt | 36 +- .../corpus-costs/6706-dh-groups.ts.json | 1 + .../6707-dh-explicit-params.ts.json | 1 + .../corpus-costs/6708-dh-errors.js.json | 1 + .../corpus-costs/6709-dh-generate.ts.json | 1 + .../corpus-costs/6710-dh-encodings.cjs.json | 1 + .../corpus-costs/6711-dh-handles.ts.json | 1 + tests/harness/errors.test.ts | 47 + tests/harness/project-config.test.ts | 14 + .../self-hosting-native-toolchain.test.ts | 1 + .../harness/self-hosting-runtime-pack.test.ts | 1 + 73 files changed, 3602 insertions(+), 309 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/builtins/dh.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6706-dh-groups.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6707-dh-explicit-params.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6708-dh-errors.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6709-dh-generate.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6710-dh-encodings.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6711-dh-handles.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/dh.ts.json create mode 100644 packages/runtime/src/scr_crypto_args.h create mode 100644 packages/runtime/src/scr_dh.c create mode 100644 packages/runtime/src/scr_dh_groups.h create mode 100644 packages/runtime/test/dh.test.ts create mode 100644 packages/runtime/test/test_dh.c create mode 100644 tests/corpus/6706-dh-groups.ts create mode 100644 tests/corpus/6707-dh-explicit-params.ts create mode 100644 tests/corpus/6708-dh-errors.js create mode 100644 tests/corpus/6709-dh-generate.ts create mode 100644 tests/corpus/6710-dh-encodings.cjs create mode 100644 tests/corpus/6711-dh-handles.ts create mode 100644 tests/fixtures/node-types/dh.ts create mode 100644 tests/harness/corpus-costs/6706-dh-groups.ts.json create mode 100644 tests/harness/corpus-costs/6707-dh-explicit-params.ts.json create mode 100644 tests/harness/corpus-costs/6708-dh-errors.js.json create mode 100644 tests/harness/corpus-costs/6709-dh-generate.ts.json create mode 100644 tests/harness/corpus-costs/6710-dh-encodings.cjs.json create mode 100644 tests/harness/corpus-costs/6711-dh-handles.ts.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 3c830c63d9..b07cd69624 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -15,5 +15,11 @@ `createECDH` and `ECDH.convertKey` support prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1, and brainpoolP512r1, on native targets, through `generateKeys()`, `computeSecret()`, `getPublicKey()`, `getPrivateKey()`, `setPrivateKey()`, and `setPublicKey()`, in compressed, uncompressed, and hybrid forms; the other curves `crypto.getCurves()` lists and wasm32-wasi builds are refused. A call on an ECDH value whose arguments no @types/node overload accepts (`getPublicKey("HEX")`) is refused at compile time, as is `computeSecret(view, encoding)`: Node reads that encoding as the input encoding and answers a Buffer where @types/node declares a string. ECDH property reads and writes throw a coded SC2020 error. + `createDiffieHellman`, `getDiffieHellman`, and `createDiffieHellmanGroup` support the modp1, modp2, modp5, and modp14 through modp18 groups, explicit primes and generators, and generated safe primes, on native targets, through `generateKeys()`, `computeSecret()`, `getPrime()`, `getGenerator()`, `getPublicKey()`, `getPrivateKey()`, `setPublicKey()`, `setPrivateKey()`, and `verifyError`, with Node's private key lengths, DH_check results, and errors; wasm32-wasi builds are refused. + Primes longer than 8192 bits throw a coded SC2020 error; Node accepts primes up to 10000 bits for key generation and secrets. + A call on a DiffieHellman value whose arguments no @types/node overload accepts, or whose output encoding is typed `any`, is refused at compile time, as are its `Object.prototype` methods; other property reads, and property writes, throw a coded SC2020 error. + `Object.keys()` and `in` do not see a DiffieHellman's own `verifyError` property, and `JSON.stringify()` and `instanceof` are refused. + After `createDiffieHellman(primeLength)` throws `Invalid DH parameters`, the next OpenSSL error Node throws can list the failed generation's errors in `opensslErrorStack`; errors in a compiled binary list only their own. + `crypto.diffieHellman`, which takes asymmetric KeyObjects, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available. Other public-key operations remain unsupported. Native derivations finish before their deferred callbacks, so computation blocks the caller. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 66b0ba3dd3..09bc7df299 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -11177,9 +11177,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Supports the modp groups, explicit primes and generators, and generated safe primes up to 8192 bits with Node's argument, generator and modulus errors.", + "verification": "test-backed" } }, { @@ -11200,9 +11200,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Supports the modp groups, explicit primes and generators, and generated safe primes up to 8192 bits with Node's argument, generator and modulus errors.", + "verification": "test-backed" } }, { @@ -11223,9 +11223,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Supports the modp groups, explicit primes and generators, and generated safe primes up to 8192 bits with Node's argument, generator and modulus errors.", + "verification": "test-backed" } }, { @@ -11247,7 +11247,7 @@ }, "static": { "status": "partial", - "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "detail": "Supports prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1 and brainpoolP512r1 with Node's curve, key, point, format and encoding errors; other curves Node knows are refused.", "verification": "test-backed" } }, @@ -11729,9 +11729,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Supports the modp groups, explicit primes and generators, and generated safe primes up to 8192 bits with Node's argument, generator and modulus errors.", + "verification": "test-backed" } }, { @@ -12856,9 +12856,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -12879,9 +12879,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -12902,9 +12902,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -12925,9 +12925,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -12948,9 +12948,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -12971,9 +12971,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -12994,9 +12994,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -13017,9 +13017,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -13040,9 +13040,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -13063,9 +13063,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -13086,9 +13086,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.", + "verification": "test-backed" } }, { @@ -18880,9 +18880,9 @@ "anchor": "err_crypto_invalid_keylen", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -19006,9 +19006,9 @@ "anchor": "err_crypto_invalid_state", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -19276,9 +19276,9 @@ "anchor": "err_crypto_unknown_dh_group", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index c6a7a102c3..7ebd09df84 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19313,11 +19313,18 @@ "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", + "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/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/fixtures/node-types/cipher.ts", + "tests/fixtures/node-types/dh.ts", "tests/fixtures/node-types/ecdh.ts", "tests/harness/npm-static.test.ts" ] @@ -19759,8 +19766,17 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.createDiffieHellman" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.createDiffieHellman", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -19782,8 +19798,17 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.createDiffieHellman" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.createDiffieHellman", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -19805,8 +19830,17 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.createDiffieHellmanGroup" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.createDiffieHellmanGroup", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -20387,8 +20421,17 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.getDiffieHellman" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.getDiffieHellman", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -22153,8 +22196,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:DiffieHellman", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -22176,8 +22228,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.computeSecret", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -22199,8 +22260,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.generateKeys", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -22222,8 +22292,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.getGenerator", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -22245,8 +22324,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.getPrime", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -22268,8 +22356,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.getPrivateKey", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -22291,8 +22388,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.getPublicKey", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -22314,8 +22420,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.setPrivateKey", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -22337,8 +22452,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.setPublicKey", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -22360,8 +22484,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:diffieHellman.verifyError", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -22383,8 +22516,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:DiffieHellmanGroup", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -27448,8 +27590,17 @@ "anchor": "dep0167-weak-diffiehellmangroup-instances-modp1-modp2-modp5", "nodeStability": null, "static": { - "status": "not-applicable", - "evidence": "compiler-chapter-policy:deprecations" + "status": "partial", + "evidence": "compiler-dedicated:DiffieHellmanGroup", + "tests": [ + "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" + ] }, "anchorSource": "exact" }, @@ -32191,6 +32342,7 @@ "evidence": "derived:descendants", "tests": [ "tests/corpus/6702-ecdh-errors.js", + "tests/corpus/6708-dh-errors.js", "tests/corpus/caught-system-error-fields.ts", "tests/corpus/error-stack-frames.ts", "tests/corpus/error-stack-limit.ts", @@ -33601,8 +33753,11 @@ "anchor": "err_crypto_invalid_keylen", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-dedicated:ERR_CRYPTO_INVALID_KEYLEN", + "tests": [ + "tests/corpus/6708-dh-errors.js" + ] }, "anchorSource": "exact" }, @@ -33727,8 +33882,11 @@ "anchor": "err_crypto_invalid_state", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-dedicated:ERR_CRYPTO_INVALID_STATE", + "tests": [ + "tests/corpus/6708-dh-errors.js" + ] }, "anchorSource": "exact" }, @@ -33997,8 +34155,11 @@ "anchor": "err_crypto_unknown_dh_group", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-dedicated:ERR_CRYPTO_UNKNOWN_DH_GROUP", + "tests": [ + "tests/corpus/6708-dh-errors.js" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index 9bd3927b8c..536d4ce5e6 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -582,17 +582,27 @@ function publicDetail(tier) { if (/^surface-manifest:node-builtin\.crypto\.create(?:Cipheriv|Decipheriv)$/.test(source)) { return "Supports aes-128/192/256 in cbc, ctr, and gcm modes and chacha20-poly1305 with Node's argument, IV, tag-length, and key-length errors; other OpenSSL ciphers and KeyObject keys are refused."; } + if (/^surface-manifest:node-builtin\.crypto\.createECDH$/.test(source)) { + return "Supports prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1 and brainpoolP512r1 with Node's curve, key, point, format and encoding errors; other curves Node knows are refused."; + } + if ( + /^surface-manifest:node-builtin\.crypto\.(?:createDiffieHellman|createDiffieHellmanGroup|getDiffieHellman)$/.test( + source, + ) + ) { + return "Supports the modp groups, explicit primes and generators, and generated safe primes up to 8192 bits with Node's argument, generator and modulus errors."; + } if (source.startsWith("surface-manifest:")) return "Implemented for the call shapes accepted by the compiler lowering."; if (/^compiler-dedicated:assert\.doesNot(?:Throw|Reject)$/.test(source)) { return "Supports callbacks or native promises with messages, Error classes, regular expressions, and validators accepting unknown values; object expectations and complete AssertionError metadata remain unsupported."; } - if (/^surface-manifest:node-builtin\.crypto\.createECDH$/.test(source)) { - return "Supports prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1 and brainpoolP512r1 with Node's curve, key, point, format and encoding errors; other curves Node knows are refused."; - } if (/^compiler-dedicated:(?:ECDH|ecdh\.)/.test(source)) { return "Matches Node's key formats, point encodings, private-key range checks, key-pair checks and errors on the supported curves; ECDH.convertKey is supported, and property reads and writes are refused."; } + if (/^compiler-dedicated:(?:DiffieHellman|diffieHellman\.)/.test(source)) { + return "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused."; + } if (/^compiler-dedicated:(?:Cipheriv|Decipheriv|cipher\.|decipher\.)/.test(source)) { return "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused."; } diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 23c7a159b6..b6cfed75d5 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -44,6 +44,8 @@ "node-builtin.console": ["tests/corpus/console-imports.cjs", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts"], "node-builtin.crypto.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/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/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"], "node-builtin.crypto": ["tests/corpus/1358-crypto-random.ts", "tests/corpus/1534-crypto-hash-chain.ts", "tests/corpus/2556-crypto-introspection.ts", "tests/corpus/2890-crypto-static-utility.ts", "tests/harness/npm-static.test.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts"], "node-builtin.dgram": ["tests/corpus/2597-dgram-send-ladders.cjs"], @@ -118,6 +120,7 @@ "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.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"], "node-builtin.url.format": ["tests/corpus/5224-url-format-objects.ts", "tests/corpus/5225-url-format-whatwg.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"], @@ -414,6 +417,8 @@ { "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] }, { "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["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/fixtures/node-types/cipher.ts"] }, { "chapter": "crypto", "symbols": ["ECDH", "ecdh.computeSecret", "ecdh.generateKeys", "ecdh.getPrivateKey", "ecdh.getPublicKey", "ecdh.setPrivateKey", "ecdh.setPublicKey"], "status": "partial", "evidence": ["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"] }, + { "chapter": "crypto", "symbols": ["DiffieHellman", "DiffieHellmanGroup", "diffieHellman.computeSecret", "diffieHellman.generateKeys", "diffieHellman.getGenerator", "diffieHellman.getPrime", "diffieHellman.getPrivateKey", "diffieHellman.getPublicKey", "diffieHellman.setPrivateKey", "diffieHellman.setPublicKey", "diffieHellman.verifyError"], "status": "partial", "evidence": ["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"] }, + { "chapter": "errors", "symbols": ["ERR_CRYPTO_UNKNOWN_DH_GROUP", "ERR_CRYPTO_INVALID_KEYLEN", "ERR_CRYPTO_INVALID_STATE"], "status": "partial", "evidence": ["tests/corpus/6708-dh-errors.js"] }, { "chapter": "errors", "symbols": ["ERR_CRYPTO_ECDH_INVALID_FORMAT", "ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY"], "status": "partial", "evidence": ["tests/corpus/6702-ecdh-errors.js"] }, { "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] }, { "symbols": ["crypto.randomFill", "crypto.randomFillSync", "crypto.randomUUID", "crypto.randomUUIDv7"], "status": "supported", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/1358-crypto-random.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index dbb3cd8fda..ab4b59eb8f 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -2968,6 +2968,68 @@ declare module "crypto" { setPrivateKey(privateKey: string, encoding: BinaryToTextEncoding): void; } export function createECDH(curveName: string): ECDH; + export class DiffieHellman { + private constructor(); + generateKeys(): Buffer; + generateKeys(encoding: BinaryToTextEncoding): string; + computeSecret( + otherPublicKey: ArrayBufferView, + inputEncoding?: null, + outputEncoding?: null, + ): Buffer; + computeSecret( + otherPublicKey: string, + inputEncoding: BinaryToTextEncoding, + outputEncoding?: null, + ): Buffer; + computeSecret( + otherPublicKey: ArrayBufferView, + inputEncoding: null, + outputEncoding: BinaryToTextEncoding, + ): string; + computeSecret( + otherPublicKey: string, + inputEncoding: BinaryToTextEncoding, + outputEncoding: BinaryToTextEncoding, + ): string; + getPrime(): Buffer; + getPrime(encoding: BinaryToTextEncoding): string; + getGenerator(): Buffer; + getGenerator(encoding: BinaryToTextEncoding): string; + getPublicKey(): Buffer; + getPublicKey(encoding: BinaryToTextEncoding): string; + getPrivateKey(): Buffer; + getPrivateKey(encoding: BinaryToTextEncoding): string; + setPublicKey(publicKey: ArrayBufferView): void; + setPublicKey(publicKey: string, encoding: BufferEncoding): void; + setPrivateKey(privateKey: ArrayBufferView): void; + setPrivateKey(privateKey: string, encoding: BufferEncoding): void; + verifyError: number; + } + export type DiffieHellmanGroup = Omit; + export function createDiffieHellman(primeLength: number, generator?: number): DiffieHellman; + export function createDiffieHellman( + prime: ArrayBuffer | ArrayBufferView, + generator?: number | ArrayBuffer | ArrayBufferView, + ): DiffieHellman; + export function createDiffieHellman( + prime: ArrayBuffer | ArrayBufferView, + generator: string, + generatorEncoding: BinaryToTextEncoding, + ): DiffieHellman; + export function createDiffieHellman( + prime: string, + primeEncoding: BinaryToTextEncoding, + generator?: number | ArrayBuffer | ArrayBufferView, + ): DiffieHellman; + export function createDiffieHellman( + prime: string, + primeEncoding: BinaryToTextEncoding, + generator: string, + generatorEncoding: BinaryToTextEncoding, + ): DiffieHellman; + export function getDiffieHellman(groupName: string): DiffieHellmanGroup; + export function createDiffieHellmanGroup(name: string): DiffieHellmanGroup; export interface ScryptOptions { N?: number; r?: number; diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index f7cae8508d..92b16965de 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -56,6 +56,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { tlsCa: features.tlsCa, cipher: features.cipher, ecdh: features.ecdh, + dh: features.dh, webcrypto: features.webcrypto, perfLoop: features.perfLoop, }; diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index 805198df11..a582036887 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -60,6 +60,7 @@ export async function compilePackedLibrary( tls: false, cipher: false, ecdh: false, + dh: false, webcrypto: false, perfLoop: false, tlsCa: false, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8bb8bc3aab..0745b88061 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -577,6 +577,8 @@ export const LIB_FN_SYMS: Record = { "crypto.cipherNew": "scr_crypto_cipher_new", "crypto.ecdhNew": "scr_crypto_ecdh_new", "crypto.ecdhConvertKey": "scr_crypto_ecdh_convert_key", + "crypto.dhNew": "scr_crypto_dh_new", + "crypto.dhGroupNew": "scr_crypto_dh_group_new", "crypto.hmacNewStr": "scr_crypto_hmac_new_str", "crypto.hmacNewBytes": "scr_crypto_hmac_new_bytes", "crypto.hashUpdateStr": "scr_crypto_hash_update_str", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index a476e1f7f2..ad8b662eab 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -90,6 +90,9 @@ export interface NativeLinkFeatures { /** crypto.createECDH and ECDH.convertKey: scr_ecdh.c plus the mbedTLS * archive. */ ecdh: boolean; + /** crypto.createDiffieHellman, getDiffieHellman and + * createDiffieHellmanGroup: scr_dh.c plus the mbedTLS archive. */ + dh: boolean; /** perf_hooks event-loop metrics: scr_perf_loop.c. */ perfLoop: boolean; tlsCa: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 06a41c9b22..f268e67b29 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -249,6 +249,10 @@ export interface CcOptions { /** crypto.createECDH and ECDH.convertKey: compiles scr_ecdh.c and links * the mbedTLS archive (shared with tls). */ ecdh?: boolean; + /** crypto.createDiffieHellman, getDiffieHellman and + * createDiffieHellmanGroup: compiles scr_dh.c and links the mbedTLS + * archive (shared with tls). */ + dh?: boolean; /** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */ webcrypto?: boolean; /** perf_hooks event-loop metrics: compiles scr_perf_loop.c. */ diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index a551ad600d..8caf79a072 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -114,6 +114,7 @@ interface RuntimeUnitContext { tlsCa: boolean; cipher: boolean; ecdh: boolean; + dh: boolean; lreObjects: readonly string[]; zlibObjects: readonly string[]; tlsArchive: string | null; @@ -308,6 +309,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ ...(u.tls ? [u.rt(join(u.rtDir, "scr_tls.c"))] : []), ...(u.cipher ? [u.rt(join(u.rtDir, "scr_cipher.c"))] : []), ...(u.ecdh ? [u.rt(join(u.rtDir, "scr_ecdh.c"))] : []), + ...(u.dh ? [u.rt(join(u.rtDir, "scr_dh.c"))] : []), u.tlsArchive, // mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h). // The unconditional win32 libs above do not carry it. @@ -372,10 +374,12 @@ export async function compileCInternal( const http = (opts.http ?? false) || nativeFetch; const tls = (opts.tls ?? false) || nativeFetch; const tlsCa = (opts.tlsCa ?? false) || tls; - // Symmetric ciphers and ECDH link the same mbedTLS archive as TLS. + // Symmetric ciphers, ECDH and DiffieHellman link the same mbedTLS + // archive as TLS. const cipher = opts.cipher ?? false; const ecdh = opts.ecdh ?? false; - const mbedtls = tls || cipher || ecdh; + const dh = opts.dh ?? false; + const mbedtls = tls || cipher || ecdh || dh; const driver = resolveCc(); const darwinDebugSymbols = needsDarwinDebugSymbols( targetPlatform(driver), @@ -733,6 +737,7 @@ export async function compileCInternal( tlsCa, cipher, ecdh, + dh, lreObjects, zlibObjects, tlsArchive, diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 19519e1f22..e2ba393a2e 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -125,6 +125,7 @@ const features = { tlsCa: false, cipher: false, ecdh: false, + dh: false, webcrypto: false, perfLoop: false, }; diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index ee2301172f..2a1c46f72d 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -61,6 +61,7 @@ const BASE: NativeLinkFeatures = { tlsCa: false, cipher: false, ecdh: false, + dh: false, webcrypto: false, perfLoop: false, }; diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts index b7049f6fe3..e480beae4f 100644 --- a/packages/compiler/src/backend/target-diagnostics.ts +++ b/packages/compiler/src/backend/target-diagnostics.ts @@ -215,6 +215,13 @@ export function moduleWasiUnavailableSurface( }; return false; } + if (node.fn === "crypto.dhNew" || node.fn === "crypto.dhGroupNew") { + found = { + surface: "DiffieHellman (the wasm32-wasi runtime does not include the mbedTLS library)", + loc, + }; + return false; + } if (node.fn === "os.networkInterfaces") { found = { surface: "network-interface enumeration (WASI Preview 1 has no interface API)", diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index 2d8ae66417..8870e21e7b 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -67,6 +67,7 @@ export async function warmNativeCaches( tls: false, cipher: false, ecdh: false, + dh: false, webcrypto: false, perfLoop: false, tlsCa: false, diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index 94b136c906..3770a6bd6f 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -65,6 +65,7 @@ const native: EarlyExecutableNativeFeatures = { tlsCa: false, cipher: false, ecdh: false, + dh: false, webcrypto: false, perfLoop: false, }; diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 2312f5e85a..6f96f8f759 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -96,6 +96,7 @@ export interface EarlyExecutableNativeFeatures { tls: boolean; cipher: boolean; ecdh: boolean; + dh: boolean; perfLoop: boolean; tlsCa: boolean; webcrypto: boolean; @@ -228,6 +229,7 @@ const BOOLEAN_NATIVE_KEYS = [ "tlsCa", "cipher", "ecdh", + "dh", "webcrypto", "perfLoop", ] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[]; diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index f4270dbe82..6ea83fce56 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -1,6 +1,7 @@ import { lowerBuiltinByteInput, errorFirstBytesCallback, errorFirstCallback } from "./arguments.js"; import { isKeyObjectValue } from "./key-object.js"; import { lowerCreateEcdh } from "./ecdh.js"; +import { lowerCreateDiffieHellman, lowerGetDiffieHellman } from "./dh.js"; import { boolLit, dynUndefinedExpr, nodeThrowExpr, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; @@ -410,6 +411,9 @@ export function lowerCryptoModuleCall( }; } if (bi.member === "createECDH") return lowerCreateEcdh(lowerer, expr); + if (bi.member === "createDiffieHellman") return lowerCreateDiffieHellman(lowerer, expr); + if (bi.member === "getDiffieHellman" || bi.member === "createDiffieHellmanGroup") + return lowerGetDiffieHellman(lowerer, expr); if (bi.member === "createHmac") { if (args.length < 1 || args.length > 3 || args.some(ts.isSpreadElement)) { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/builtins/dh.ts b/packages/compiler/src/frontend/lowering/builtins/dh.ts new file mode 100644 index 0000000000..eea5a48136 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/dh.ts @@ -0,0 +1,166 @@ +import * as ts from "../../ts7/adapter.js"; +import { nodeThrowExpr } from "../../../ir/build.js"; +import { type Lowerer } from "../lowerer.js"; +import { locOf } from "../../program.js"; +import { DYN, type IrExpr, type IrStmt } from "../../../ir/ir.js"; +import { v8CalleeText } from "./crypto.js"; +import { dynArguments, refuseUnmatchedOverload } from "./ecdh.js"; + +/* node:crypto's DiffieHellman and DiffieHellmanGroup are native + * checked-dynamic handles (scr_dh.c): both types map to dyn, their methods + * dispatch through the handle, and these helpers lower + * createDiffieHellman, getDiffieHellman, createDiffieHellmanGroup and the + * method calls. The runtime applies Node's argument checks, so the + * constructors take any arguments; method calls are refused when their + * argument types leave the Buffer or string result undetermined. */ + +/** Whether a checker type is node:crypto's DiffieHellman class or its + * DiffieHellmanGroup alias, or a union of them. */ +export function isDiffieHellmanType(lowerer: Lowerer, type: ts.Type): boolean { + if (type.isUnionType()) + return ts.constituentTypes(type).every((part) => isDiffieHellmanType(lowerer, part)); + const symbol = type.getAliasSymbol() ?? type.getSymbol(); + if (!symbol || !lowerer.isStdlibSymbol(symbol)) return false; + const name = ts.symbolName(symbol); + return name === "DiffieHellman" || name === "DiffieHellmanGroup"; +} + +function isDiffieHellmanGroupType(type: ts.Type): boolean { + if (type.isUnionType()) return ts.constituentTypes(type).some(isDiffieHellmanGroupType); + const symbol = type.getAliasSymbol(); + return !!symbol && ts.symbolName(symbol) === "DiffieHellmanGroup"; +} + +function newHandle( + lowerer: Lowerer, + call: ts.CallExpression, + fn: "crypto.dhNew" | "crypto.dhGroupNew", +): IrExpr { + const loc = locOf(call); + const stmts: IrStmt[] = []; + const values = dynArguments(lowerer, call, fn === "crypto.dhNew" ? 4 : 1, stmts, "%dhInput"); + return { + kind: "seqExpr", + stmts, + result: { kind: "libCall", fn, args: values, type: DYN, loc }, + type: DYN, + loc, + }; +} + +/** crypto.createDiffieHellman(sizeOrKey, keyEncoding, generator, + * generatorEncoding): a native DiffieHellman handle. */ +export function lowerCreateDiffieHellman(lowerer: Lowerer, call: ts.CallExpression): IrExpr { + return newHandle(lowerer, call, "crypto.dhNew"); +} + +/** crypto.getDiffieHellman(groupName) and createDiffieHellmanGroup(name): + * a native DiffieHellmanGroup handle. */ +export function lowerGetDiffieHellman(lowerer: Lowerer, call: ts.CallExpression): IrExpr { + return newHandle(lowerer, call, "crypto.dhGroupNew"); +} + +const DH_METHODS = new Set([ + "generateKeys", + "computeSecret", + "getPrime", + "getGenerator", + "getPublicKey", + "getPrivateKey", + "setPublicKey", + "setPrivateKey", +]); + +/** Object.prototype methods, which a DiffieHellman inherits. */ +const OBJECT_METHODS = new Set([ + "constructor", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "toLocaleString", + "toString", + "valueOf", +]); + +/** The argument that selects each method's Buffer or string result: the + * output encoding. */ +const OUTPUT_ENCODING_ARG: Record = { + generateKeys: 0, + computeSecret: 2, + getPrime: 0, + getGenerator: 0, + getPublicKey: 0, + getPrivateKey: 0, +}; + +/** An output encoding typed any: the checker's chosen overload then types + * the result as a Buffer or a string that Node may not answer, so the call + * is refused rather than checked against that type at run time. */ +function refuseAnyOutputEncoding(lowerer: Lowerer, call: ts.CallExpression, name: string): void { + const index = OUTPUT_ENCODING_ARG[name]; + const arg = index === undefined ? undefined : call.arguments.nodes[index]; + if (!arg || (lowerer.typeOf(arg).flags & ts.TypeFlags.Any) === 0) return; + lowerer.noLowering( + `DiffieHellman.${name} with an output encoding typed any`, + arg, + "type the encoding as a BinaryToTextEncoding such as 'hex', or as null or undefined for a Buffer", + ); +} + +/** Calls on native DiffieHellman and DiffieHellmanGroup handles. Methods + * dispatch through the handle with checked-dynamic arguments. */ +export function lowerDiffieHellmanCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null; + const access = call.expression; + if (access.questionDotToken) return null; + const receiver = lowerer.typeOf(access.expression); + if (!isDiffieHellmanType(lowerer, receiver)) return null; + const name = access.name.text; + const loc = locOf(call); + const setter = name === "setPublicKey" || name === "setPrivateKey"; + if (!DH_METHODS.has(name) || (setter && isDiffieHellmanGroupType(receiver))) { + if (OBJECT_METHODS.has(name)) + lowerer.noLowering( + `DiffieHellman.${name}`, + call, + "generateKeys(), computeSecret(), getPrime(), getGenerator(), getPublicKey(), getPrivateKey(), " + + "setPublicKey(), setPrivateKey() and verifyError are supported", + ); + // Node reads the member as undefined (a DiffieHellmanGroup has no key + // setters) and throws a TypeError once the receiver and arguments are + // evaluated. + const evaluated = [access.expression, ...call.arguments.nodes].map((node): IrStmt => ({ + kind: "exprStmt", + expr: lowerer.lowerExpr(node), + loc, + })); + return { + kind: "seqExpr", + stmts: evaluated, + result: nodeThrowExpr(1, "", `${v8CalleeText(access)} is not a function`, DYN, loc), + type: DYN, + loc, + }; + } + if (call.arguments.nodes.some(ts.isSpreadElement)) + lowerer.noLowering(`DiffieHellman.${name} with spread arguments`, call); + refuseAnyOutputEncoding(lowerer, call, name); + refuseUnmatchedOverload(lowerer, call, `DiffieHellman.${name}`); + const invoke: IrExpr = { + kind: "dynInvoke", + recv: lowerer.lowerExprExpecting(access.expression, DYN), + method: name, + calleeName: v8CalleeText(access), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + type: DYN, + loc, + }; + // setPublicKey and setPrivateKey answer the receiver where @types/node + // declares void. Other results take the static representation of the + // declared overload the arguments match. + if (setter) return invoke; + const result = lowerer.irTypeOf(call); + return result.kind === "bytes" || result.kind === "string" + ? lowerer.coerceInto(call, invoke, result) + : invoke; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/ecdh.ts b/packages/compiler/src/frontend/lowering/builtins/ecdh.ts index e08be56f45..6e937284ce 100644 --- a/packages/compiler/src/frontend/lowering/builtins/ecdh.ts +++ b/packages/compiler/src/frontend/lowering/builtins/ecdh.ts @@ -28,15 +28,16 @@ export function isEcdhType(lowerer: Lowerer, type: ts.Type): boolean { return !!symbol && ts.symbolName(symbol) === "ECDH" && lowerer.isStdlibSymbol(symbol); } -/** The call's arguments as checked-dynamic values in hidden locals, the - * first `count` of them padded with undefined, so every argument is - * evaluated left to right before the runtime checks them. Surplus - * arguments are evaluated and dropped. */ -function dynArguments( +/** The call's arguments as checked-dynamic values in hidden locals named + * `local`, the first `count` of them padded with undefined, so every + * argument is evaluated left to right before the runtime checks them. + * Surplus arguments are evaluated and dropped. */ +export function dynArguments( lowerer: Lowerer, call: ts.CallExpression, count: number, stmts: IrStmt[], + local = "%ecdhInput", ): IrExpr[] { const args = call.arguments.nodes; const loc = locOf(call); @@ -45,7 +46,7 @@ function dynArguments( const input: IrExpr = args[i] ? lowerer.lowerExprExpecting(args[i]!, DYN) : dynUndefinedExpr(loc); - const saved = lowerer.declareHiddenLocal("%ecdhInput", DYN); + const saved = lowerer.declareHiddenLocal(local, DYN); stmts.push({ kind: "varDecl", localId: saved.id, init: input, loc }); values.push(varRef(saved.id, DYN, loc)); } @@ -137,7 +138,11 @@ function refuseViewKeyWithEncoding(lowerer: Lowerer, call: ts.CallExpression, na * not follow (getPublicKey("HEX") answers a string where the encoding-less * overload declares a Buffer), so the call is refused rather than checked * against that type at run time. Arguments typed any are accepted. */ -function refuseUnmatchedOverload(lowerer: Lowerer, call: ts.CallExpression, what: string): void { +export function refuseUnmatchedOverload( + lowerer: Lowerer, + call: ts.CallExpression, + what: string, +): void { const signature = lowerer.checker.getResolvedSignature(call); if (signature === undefined) return; const params = signature.getParameters(); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 870e21b1f8..8072e410d2 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -58,6 +58,7 @@ import { encodeBase64 } from "../../wasm/base64.js"; import { resolveRequireRuntime } from "../runtime-resolve.js"; import { lowerKeyObjectCall } from "./builtins/key-object.js"; import { isEcdhType, lowerEcdhCall } from "./builtins/ecdh.js"; +import { isDiffieHellmanType, lowerDiffieHellmanCall } from "./builtins/dh.js"; import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js"; import { hashStreamOf } from "./builtins/hash-stream.js"; import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js"; @@ -3620,9 +3621,9 @@ export class Lowerer { * is `unknown` or `object` (also with `null` or `undefined`, or an array * of those), names checked protocol identity, is an owned runtime * handle (a Worker or MessagePort, a fetch Headers/Request/Response, or - * a node:crypto ECDH), or is a WeakMap/WeakSet (native weak collections - * live in checked-dynamic storage, as locals of those types do); `any` - * fields stay refused. */ + * a node:crypto ECDH, DiffieHellman or DiffieHellmanGroup), or is a + * WeakMap/WeakSet (native weak collections live in checked-dynamic + * storage, as locals of those types do); `any` fields stay refused. */ classFieldStoresDyn(type: ts.Type): boolean { return ( this.opaqueValueType(type) || @@ -3630,7 +3631,10 @@ export class Lowerer { isWorkerHandleType(type, this.typeCtx) || isFetchHandleType(type, this.typeCtx) || this.weakCollectionType(type) || - this.stdlibHandleType(type, (part) => isEcdhType(this, part)) + this.stdlibHandleType( + type, + (part) => isEcdhType(this, part) || isDiffieHellmanType(this, part), + ) ); } @@ -12737,6 +12741,7 @@ export class Lowerer { lowerWebCryptoCall(this, expr) ?? lowerKeyObjectCall(this, expr) ?? lowerEcdhCall(this, expr) ?? + lowerDiffieHellmanCall(this, expr) ?? lowerNetAddressCall(this, expr) ?? lowerFetchElementMethodCall(this, expr) ?? lowerDynamicImportCall(this, expr) ?? diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index c779383376..75bf425168 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1574,6 +1574,12 @@ export const BUILTIN_MODULE_FNS: Record< // A checked-dynamic curve name; the result is a native ECDH handle // (lowerCreateEcdh). createECDH: { fn: "crypto.ecdhNew", params: [DYN], result: DYN }, + // Checked-dynamic arguments; the result is a native DiffieHellman or + // DiffieHellmanGroup handle (lowerCreateDiffieHellman and + // lowerGetDiffieHellman). + 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 }, }, zlib: { // The default-options convenience family is special-cased in @@ -3063,10 +3069,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< "createVerify", "sign", "verify", - "createDiffieHellman", - "createDiffieHellmanGroup", - "getDiffieHellman", - "diffieHellman", "publicEncrypt", "publicDecrypt", "privateEncrypt", @@ -3078,6 +3080,9 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< "can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics", ]), ), + diffieHellman: + "crypto.diffieHellman takes asymmetric KeyObjects, which the static runtime does not have; " + + "the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets", ...Object.fromEntries( ["getCipherInfo"].map((m) => [ m, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index a233f0186d..189efaac03 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -2973,6 +2973,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ) { return DYN; } + // node:crypto DiffieHellman values, and @types/node's DiffieHellmanGroup + // alias over them, are native handles carried by the checked-dynamic + // representation; lowerDiffieHellmanCall dispatches their methods. + { + const dhSym = widened.getAliasSymbol() ?? psym; + if ( + dhSym !== undefined && + (ts.symbolName(dhSym) === "DiffieHellman" || ts.symbolName(dhSym) === "DiffieHellmanGroup") && + dhSym.declarations.some( + (d) => + (ts.isClassDeclaration(d) || ts.isTypeAliasDeclaration(d)) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), + ) + ) { + return DYN; + } + } // ReadonlyMap/ReadonlySet are the SAME runtime values as Map/Set — the // readonly-ness is a checker-only view (no mutating members on the // interface), so both map to the identical IR kinds and the read-side diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 92ee6b75eb..acd9f168ab 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -620,6 +620,7 @@ async function compileExecutableNative( tlsCa: features.tlsCa, cipher: features.cipher, ecdh: features.ecdh, + dh: features.dh, webcrypto: features.webcrypto, perfLoop: features.perfLoop, ...(onArtifactReady === undefined ? {} : { onArtifactReady }), diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index ca8c521186..21215d28f1 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -13,6 +13,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // Node's curve, key, point, encoding and format errors. "crypto.ecdhNew", "crypto.ecdhConvertKey", + // Node's prime, generator, size, encoding and group-name errors. + "crypto.dhNew", + "crypto.dhGroupNew", // 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 771b531524..d362e13fac 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2782,6 +2782,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** ECDH.convertKey(key, curve, inputEncoding, outputEncoding, format): * the converted point as a Buffer or string. */ "crypto.ecdhConvertKey": { argTypes: [DYN, DYN, DYN, DYN, DYN], result: DYN }, + /** createDiffieHellman(sizeOrKey, keyEncoding, generator, + * generatorEncoding) over checked-dynamic arguments (scr_dh.c): a native + * DiffieHellman handle whose methods dispatch through dynInvoke. */ + "crypto.dhNew": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, + /** getDiffieHellman(groupName) and createDiffieHellmanGroup(name): a + * native DiffieHellmanGroup handle. */ + "crypto.dhGroupNew": { argTypes: [DYN], result: DYN }, "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 4142b6efe3..eec599e94e 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -744,6 +744,9 @@ 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 + * the inspect unit. */ + dh: rule({ fns: ["crypto.dhNew", "crypto.dhGroupNew"] }), emitter: rule({ initial: (mod) => mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS), @@ -789,6 +792,8 @@ const RUNTIME_FEATURE_RULES = { "crypto.cipherNew", "crypto.ecdhNew", "crypto.ecdhConvertKey", + "crypto.dhNew", + "crypto.dhGroupNew", ...BUILTIN_CLASS_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, ], @@ -910,6 +915,7 @@ function noRuntimeFeatures(): RuntimeFeatures { dynInvoke: false, dynTier: false, ecdh: false, + dh: false, emitter: false, eventTarget: false, eventsOnceSignal: false, diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index 51aeb6146e..4fc912ea61 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -52,6 +52,8 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["tls.", "the node:tls surface"], ["crypto.cipherNew", "crypto.createCipheriv and createDecipheriv"], ["crypto.ecdh", "crypto.createECDH and ECDH.convertKey"], + ["crypto.dhNew", "crypto.createDiffieHellman"], + ["crypto.dhGroupNew", "crypto.getDiffieHellman and createDiffieHellmanGroup"], ["webcrypto.", "WebCrypto (crypto.subtle)"], ["fs.watch", "fs.watch"], ["watcher.", "fs.watch"], diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index ed8d52293f..8529678ff6 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -110,6 +110,7 @@ async function main(): Promise { tls: false, cipher: false, ecdh: false, + dh: false, webcrypto: false, perfLoop: false, tlsCa: false, diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index b90760616c..1a1263035c 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -161,6 +161,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "tlsCa", "cipher", "ecdh", + "dh", "webcrypto", "perfLoop", ].every((name) => typeof features[name] === "boolean"); diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index e6beb24ccc..ec9580ccc5 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -120,6 +120,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( tlsCa: false, cipher: false, ecdh: false, + dh: false, webcrypto: false, perfLoop: false, nativeFetch: false, diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index c05e994a8f..6998a574e2 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -496,17 +496,13 @@ "id": "node-builtin.crypto.createDiffieHellman", "kind": "node-builtin", "name": "crypto.createDiffieHellman", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.createDiffieHellmanGroup", "kind": "node-builtin", "name": "crypto.createDiffieHellmanGroup", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.createECDH", @@ -570,7 +566,7 @@ "name": "crypto.diffieHellman", "status": "unsupported", "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "note": "crypto.diffieHellman takes asymmetric KeyObjects, which the static runtime does not have; the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets" }, { "id": "node-builtin.crypto.generateKey", @@ -612,9 +608,7 @@ "id": "node-builtin.crypto.getDiffieHellman", "kind": "node-builtin", "name": "crypto.getDiffieHellman", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.getRandomValues", diff --git a/packages/compiler/test/cc-driver.test.ts b/packages/compiler/test/cc-driver.test.ts index b9f0403c68..5ddae6534f 100644 --- a/packages/compiler/test/cc-driver.test.ts +++ b/packages/compiler/test/cc-driver.test.ts @@ -291,6 +291,11 @@ test.skipIf(!zigOnPath())( options: { ecdh: true, dynAsync: true, dynInvoke: true, inspect: true }, call: "scr_dyn_release(scr_crypto_ecdh_new(scr_dyn_undefined()))", }, + { + name: "dh", + options: { dh: true, dynInvoke: true, inspect: true }, + call: "scr_dyn_release(scr_crypto_dh_group_new(scr_dyn_undefined()))", + }, ]; try { await withCcEnv("zigcc", "x86_64-windows-gnu", async () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6706-dh-groups.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6706-dh-groups.ts.json new file mode 100644 index 0000000000..d8f5b48c3d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6706-dh-groups.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6706-dh-groups.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6707-dh-explicit-params.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6707-dh-explicit-params.ts.json new file mode 100644 index 0000000000..4cb3c882c8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6707-dh-explicit-params.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6707-dh-explicit-params.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6708-dh-errors.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6708-dh-errors.js.json new file mode 100644 index 0000000000..c252657a8f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6708-dh-errors.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6708-dh-errors.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6709-dh-generate.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6709-dh-generate.ts.json new file mode 100644 index 0000000000..5c50c8ec18 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6709-dh-generate.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6709-dh-generate.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6710-dh-encodings.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6710-dh-encodings.cjs.json new file mode 100644 index 0000000000..86abbb6bab --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6710-dh-encodings.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6710-dh-encodings.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6711-dh-handles.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6711-dh-handles.ts.json new file mode 100644 index 0000000000..ae8daf9f1b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6711-dh-handles.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6711-dh-handles.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/dh.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/dh.ts.json new file mode 100644 index 0000000000..db078292a3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/dh.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/dh.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index db72284f69..16fe348f23 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -43,7 +43,7 @@ export const RUNTIME_PACK_MATRIX = withLibraryRuntimeFlavors( { id: "quickjs", predicate: false }, { id: "libregexp", predicate: "regex" }, { id: "zlib", predicate: "zlibEffective" }, - { id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh") }, + { id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh", "dh") }, ], system_libraries: [{ name: "System", predicate: true }], }), diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 856de4b86c..e44ba4a33b 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -172,7 +172,8 @@ async function build() { ...(unit.source === "scr_regex.c" ? ["-I", quickjs] : []), ...(unit.source === "scr_tls.c" || unit.source === "scr_cipher.c" || - unit.source === "scr_ecdh.c" + unit.source === "scr_ecdh.c" || + unit.source === "scr_dh.c" ? ["-I", join(mbedtls, "include")] : []), ...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c" diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 0f40218a18..ad426c9531 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -149,6 +149,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_tls.c", "tlsEffective"], ["scr_cipher.c", "cipher"], ["scr_ecdh.c", "ecdh"], + ["scr_dh.c", "dh"], ["scr_wasm.c", "wasm"], ["scr_webcrypto.c", "webcrypto"], ["scr_fetch.c", "nativeFetch"], @@ -212,7 +213,7 @@ export function createRuntimePackMatrix({ { id: "quickjs", predicate: false }, { id: "libregexp", predicate: "regex" }, { id: "zlib", predicate: "zlibEffective" }, - { id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh") }, + { id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh", "dh") }, ]; const matrix = withLibraryRuntimeFlavors( withWorkerRuntimeVariants({ diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index fa84e9c4d2..b99bd84384 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -223,7 +223,8 @@ async function build() { ...(unit.source === "scr_regex.c" ? ["-I", quickjs] : []), ...(unit.source === "scr_tls.c" || unit.source === "scr_cipher.c" || - unit.source === "scr_ecdh.c" + unit.source === "scr_ecdh.c" || + unit.source === "scr_dh.c" ? ["-I", join(mbedtls, "include")] : []), ...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c" diff --git a/packages/runtime-wasm32-wasi/scripts/build.mjs b/packages/runtime-wasm32-wasi/scripts/build.mjs index 2dd9839731..818ebac947 100644 --- a/packages/runtime-wasm32-wasi/scripts/build.mjs +++ b/packages/runtime-wasm32-wasi/scripts/build.mjs @@ -22,6 +22,7 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ "scr_tls.c", "scr_cipher.c", "scr_ecdh.c", + "scr_dh.c", "scr_wasm.c", "scr_fetch.c", ], diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index d12858abfa..65a6245ef0 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -94,6 +94,13 @@ ScrDyn *scr_crypto_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * or result, or NULL with Node's error pending. */ ScrDyn *scr_crypto_ecdh_new(ScrDyn *curve); ScrDyn *scr_crypto_ecdh_convert_key(ScrDyn *key, ScrDyn *curve, ScrDyn *in_enc, ScrDyn *out_enc, ScrDyn *format); +/* crypto.createDiffieHellman(sizeOrKey, keyEncoding, generator, + * generatorEncoding) and crypto.getDiffieHellman(groupName) over + * checked-dynamic arguments (scr_dh.c, linked with the vendored mbedTLS + * archive). +1 DiffieHellman or DiffieHellmanGroup handle, or NULL with + * 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); 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_crypto_args.h b/packages/runtime/src/scr_crypto_args.h new file mode 100644 index 0000000000..b8bdcc1c97 --- /dev/null +++ b/packages/runtime/src/scr_crypto_args.h @@ -0,0 +1,181 @@ +/* Argument and result handling shared by the checked-dynamic node:crypto + * key-exchange handles (scr_ecdh.c and scr_dh.c), as Node 24's + * lib/internal/crypto/diffiehellman.js and lib/internal/crypto/util.js + * read keys and encodings: getArrayBufferOrView for key inputs and + * encode() for Buffer results. `subject` names the value in the coded + * SC2020 error for encodings that have no lowering ("An ECDH encoding or + * format"). All arguments are borrowed. */ +#ifndef SCR_CRYPTO_ARGS_H +#define SCR_CRYPTO_ARGS_H + +#include "scr_runtime.h" + +#include + +/* An object's toString the program defined, as an own or inherited + * function property. */ +static bool ca_custom_to_string(ScrDyn *value) { + if (value->kind != SCR_DYN_OBJ && value->kind != SCR_DYN_TYPED_REF) return false; + ScrDyn *member = scr_dyn_obj_read(value, "toString", 8); + bool custom = member && member->kind == SCR_DYN_FUNC; + scr_dyn_release(member); + return custom; +} + +/* Whether value is a class instance whose class defines toString. */ +static bool ca_class_to_string(const ScrDyn *value) { + ScrStr *name = scr_str_new("toString", 8); + bool method = scr_dyn_typed_ref_class_has(value, name); + scr_str_release(name); + return method; +} + +/* `prefix` and the value as util.format's %s renders it: strings and + * objects with a program-defined toString as String(value), numbers as + * util.inspect does, other values inspected at depth 0. Functions and + * class instances that define toString have no lowering. */ +static void ca_throw_formatted(const char *subject, const char *code, const char *prefix, const ScrDyn *value) { + ScrDyn *v = (ScrDyn *)value; + if (v->kind == SCR_DYN_FUNC || ca_class_to_string(v)) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, subject); + scr_jb_puts(&b, " that is a function or an instance of a class defining toString has no lowering; " + "pass the name as a string"); + ScrStr *message = scr_jb_finish(&b); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + scr_str_release(message); + return; + } + ScrStr *text; + if (v->kind == SCR_DYN_STR || ca_custom_to_string(v)) text = scr_dyn_string_coerce_js(v); + else if (v->kind == SCR_DYN_NUM) text = scr_insp_f64(v->v.num); + else text = scr_insp_dyn_s(v, 0); + if (!text) return; + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, prefix); + scr_jb_put_str(&b, text); + scr_str_release(text); + ScrStr *message = scr_jb_finish(&b); + scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, code); + scr_str_release(message); +} + +static bool ca_ascii_eq_ci(const char *a, size_t a_len, const char *b) { + size_t b_len = strlen(b); + if (a_len != b_len) return false; + for (size_t i = 0; i < a_len; i++) { + char x = a[i]; + if (x >= 'A' && x <= 'Z') x = (char)(x - 'A' + 'a'); + if (x != b[i]) return false; + } + return true; +} + +static bool ca_str_is(const ScrStr *text, const char *literal) { + size_t len = strlen(literal); + return text->len == len && memcmp(text->data, literal, len) == 0; +} + +/* Node's normalizeEncoding: the canonical name, or NULL. */ +static const char *ca_normalize_encoding(const ScrStr *name) { + static const struct { const char *from; const char *to; } map[] = { + {"utf8", "utf8"}, {"utf-8", "utf8"}, {"hex", "hex"}, + {"base64", "base64"}, {"base64url", "base64url"}, {"latin1", "latin1"}, + {"binary", "latin1"}, {"ascii", "ascii"}, {"utf16le", "utf16le"}, + {"utf-16le", "utf16le"}, {"ucs2", "utf16le"}, {"ucs-2", "utf16le"}, + }; + for (size_t i = 0; i < sizeof map / sizeof map[0]; i++) + if (ca_ascii_eq_ci(name->data, name->len, map[i].from)) return map[i].to; + return NULL; +} + +/* The value a typed-ref box holds when it is a string, view, ArrayBuffer + * or native handle (+1). Other values are retained as passed, so argument + * errors describe the original value. NULL after throwing. */ +static ScrDyn *ca_unbox(ScrDyn *value) { + if (value->kind != SCR_DYN_TYPED_REF) return scr_dyn_retain(value); + ScrDyn *view = scr_dyn_typed_ref_materialize(value); + if (!view) return NULL; + if (view->kind == SCR_DYN_STR || view->kind == SCR_DYN_BYTES || view->kind == SCR_DYN_HANDLE || + scr_buffer_storage_is(view)) + return view; + scr_dyn_release(view); + return scr_dyn_retain(value); +} + +/* Buffer.from(text, encoding) as toBuf calls it: an encoding that is not + * a non-empty string, or is 'buffer', reads as utf8. +1 or NULL after + * throwing. */ +static ScrBytes *ca_string_bytes(const char *subject, const ScrStr *text, const ScrDyn *encoding) { + const char *name = "utf8"; + if (encoding->kind == SCR_DYN_STR && encoding->v.str->len > 0 && !ca_str_is(encoding->v.str, "buffer")) { + name = ca_normalize_encoding(encoding->v.str); + if (!name) { + ca_throw_formatted(subject, "ERR_UNKNOWN_ENCODING", "Unknown encoding: ", encoding); + return NULL; + } + } + ScrStr *canonical = scr_str_new(name, strlen(name)); + ScrBytes *bytes = scr_bytes_from_str(text, canonical); + scr_str_release(canonical); + return bytes; +} + +/* getArrayBufferOrView(key, 'key', encoding): a string is Buffer.from(key, + * encoding) with 'buffer' read as utf8; views and ArrayBuffers are their + * bytes. +1 or NULL after throwing. */ +static ScrBytes *ca_key_bytes(const char *subject, const ScrDyn *key, const ScrDyn *encoding) { + if (key->kind == SCR_DYN_STR) return ca_string_bytes(subject, key->v.str, encoding); + if (key->kind == SCR_DYN_BYTES || scr_buffer_storage_is(key)) return scr_bytes_buffer_source(key); + scr_dyn_arg_type_fail("key", "of type string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView", key); + return NULL; +} + +/* encode(buffer, encoding): the Buffer, or buffer.toString(encoding) for + * a truthy encoding other than 'buffer'. Consumes bytes. */ +static ScrDyn *ca_encode(const char *subject, ScrBytes *bytes, const ScrDyn *encoding) { + if (!scr_dyn_truthy(encoding) || (encoding->kind == SCR_DYN_STR && ca_str_is(encoding->v.str, "buffer"))) { + ScrDyn *result = scr_dyn_new_buffer(bytes); + scr_bytes_release(bytes); + return result; + } + /* toString answers "" for no bytes before it reads the encoding. */ + ScrStr *text = NULL; + if (bytes->len == 0) { + text = scr_str_new("", 0); + } else { + /* getEncodingOps reads `encoding + ''`. */ + ScrStr *name = encoding->kind == SCR_DYN_STR ? scr_str_retain(encoding->v.str) : NULL; + if (!name) { + if (encoding->kind == SCR_DYN_FUNC || ca_class_to_string(encoding)) { + ca_throw_formatted(subject, "ERR_UNKNOWN_ENCODING", "Unknown encoding: ", encoding); + scr_bytes_release(bytes); + return NULL; + } + name = scr_dyn_string_coerce_js(encoding); + if (!name) { + scr_bytes_release(bytes); + return NULL; + } + } + const char *canonical = ca_normalize_encoding(name); + scr_str_release(name); + if (!canonical) { + ca_throw_formatted(subject, "ERR_UNKNOWN_ENCODING", "Unknown encoding: ", encoding); + scr_bytes_release(bytes); + return NULL; + } + ScrStr *canonical_text = scr_str_new(canonical, strlen(canonical)); + text = scr_bytes_to_str(bytes, canonical_text); + scr_str_release(canonical_text); + } + scr_bytes_release(bytes); + if (!text) return NULL; + ScrDyn *result = scr_dyn_new_str(text); + scr_str_release(text); + return result; +} + +#endif diff --git a/packages/runtime/src/scr_dh.c b/packages/runtime/src/scr_dh.c new file mode 100644 index 0000000000..c514b45ba6 --- /dev/null +++ b/packages/runtime/src/scr_dh.c @@ -0,0 +1,1186 @@ +/* node:crypto DiffieHellman over the vendored mbedTLS bignum arithmetic: + * crypto.createDiffieHellman, crypto.getDiffieHellman and + * crypto.createDiffieHellmanGroup. A DiffieHellman or DiffieHellmanGroup + * is a native dyn handle. Construction follows Node 24's + * lib/internal/crypto/diffiehellman.js argument handling and + * src/crypto/crypto_dh.cc; key generation, verifyError, the public key + * range check and computeSecret follow the OpenSSL 3 paths of + * deps/ncrypto and OpenSSL 3.5's DH provider: + * - a group draws modp5 through modp18 private keys of a fixed width + * (200 to 400 bits) and modp1 and modp2 keys in [2, p - 2], and its + * verifyError is 0; + * - createDiffieHellman parameters that equal an OpenSSL named group + * draw keys below min(2^N, q) for the group's key length N; other + * parameters draw keys of bits(p) - 2 bits, and key generation and + * computeSecret need a 512- to 10000-bit prime; + * - createDiffieHellman(primeLength, generator) generates a safe prime + * p ≡ 23 (mod 24) for the generator 2, p ≡ 59 (mod 60) for 5 and + * p ≡ 11 (mod 12) otherwise, as OpenSSL's dh_builtin_genparams does. + * Primes longer than mbedTLS's 8192-bit limit throw a coded SC2020 + * error. All arguments are borrowed. */ +#include "scr_runtime.h" + +#include +#include +#include +#include +#include + +#include "mbedtls/bignum.h" +#include "mbedtls/platform_util.h" + +#include "scr_crypto_args.h" +#include "scr_dh_groups.h" + +/* The value the SC2020 error for an unlowered encoding names. */ +#define SD_SUBJECT "A DiffieHellman encoding" + +/* DH_MIN_MODULUS_BITS and OPENSSL_DH_MAX_MODULUS_BITS. */ +#define SD_MIN_MODULUS_BITS 512 +#define SD_MAX_MODULUS_BITS 10000 + +/* DH_check result bits, as verifyError reports them. */ +enum { + SD_P_NOT_PRIME = 0x01, + SD_P_NOT_SAFE_PRIME = 0x02, + SD_NOT_SUITABLE_GENERATOR = 0x08, + SD_MODULUS_TOO_SMALL = 0x80, +}; + +/* A crypto.getDiffieHellman group. */ +typedef struct { + const char *name; + const unsigned char *p; + size_t p_len; + int private_bits; /* ncrypto's GetDhGroupPrivateBits; 0 draws keys in [2, p - 2] */ +} ScDhGroup; + +static const ScDhGroup sd_groups[] = { + {"modp1", scr_dh_modp1_p, sizeof scr_dh_modp1_p, 0}, + {"modp2", scr_dh_modp2_p, sizeof scr_dh_modp2_p, 0}, + {"modp5", scr_dh_modp5_p, sizeof scr_dh_modp5_p, 200}, + {"modp14", scr_dh_modp14_p, sizeof scr_dh_modp14_p, 225}, + {"modp15", scr_dh_modp15_p, sizeof scr_dh_modp15_p, 275}, + {"modp16", scr_dh_modp16_p, sizeof scr_dh_modp16_p, 325}, + {"modp17", scr_dh_modp17_p, sizeof scr_dh_modp17_p, 375}, + {"modp18", scr_dh_modp18_p, sizeof scr_dh_modp18_p, 400}, +}; + +/* An OpenSSL DH named group, which the DH provider recognizes in explicit + * p and g. A NULL q is (p - 1) / 2. */ +typedef struct { + const unsigned char *p; + size_t p_len; + const unsigned char *g; + size_t g_len; + const unsigned char *q; + size_t q_len; + int keylength; +} ScDhNamed; + +static const unsigned char sd_two[] = {2}; + +#define SD_SAFE(p, keylength) {p, sizeof p, sd_two, 1, NULL, 0, keylength} +#define SD_RFC5114(name, keylength) \ + {scr_dh_##name##_p, sizeof scr_dh_##name##_p, scr_dh_##name##_g, sizeof scr_dh_##name##_g, \ + scr_dh_##name##_q, sizeof scr_dh_##name##_q, keylength} + +static const ScDhNamed sd_named[] = { + SD_SAFE(scr_dh_ffdhe2048_p, 225), SD_SAFE(scr_dh_ffdhe3072_p, 275), SD_SAFE(scr_dh_ffdhe4096_p, 325), + SD_SAFE(scr_dh_ffdhe6144_p, 375), SD_SAFE(scr_dh_ffdhe8192_p, 400), SD_SAFE(scr_dh_modp5_p, 200), + SD_SAFE(scr_dh_modp14_p, 225), SD_SAFE(scr_dh_modp15_p, 275), SD_SAFE(scr_dh_modp16_p, 325), + SD_SAFE(scr_dh_modp17_p, 375), SD_SAFE(scr_dh_modp18_p, 400), SD_RFC5114(dh_1024_160, 160), + SD_RFC5114(dh_2048_224, 224), SD_RFC5114(dh_2048_256, 224), +}; + +typedef struct ScDh { + size_t rc; + const ScDhGroup *group; /* crypto.getDiffieHellman's group, or NULL */ + const ScDhNamed *named; /* the named group createDiffieHellman's parameters equal, or NULL */ + mbedtls_mpi p, g; + mbedtls_mpi pub; /* when has_pub */ + mbedtls_mpi priv; /* when has_priv */ + bool has_pub; + bool has_priv; + int verify_error; /* -1 until computed */ +} ScDh; + +static ScrDynHandleOps sd_ops; +static ScrDynHandleOps sd_group_ops; + +static void *sd_retain(void *h) { + ((ScDh *)h)->rc++; + return h; +} + +static void sd_release(void *h) { + ScDh *d = h; + if (--d->rc) return; + mbedtls_mpi_free(&d->p); + mbedtls_mpi_free(&d->g); + mbedtls_mpi_free(&d->pub); + mbedtls_mpi_free(&d->priv); + mbedtls_platform_zeroize(d, sizeof *d); + free(d); +} + +static int sd_random(void *state, unsigned char *out, size_t len) { + (void)state; + arc4random_buf(out, len); + return 0; +} + +/* ── errors ───────────────────────────────────────────────────────────── */ + +static void sd_throw(int kind, const char *code, const char *message) { + scr_throw_error_msg_code(kind, message, strlen(message), code); +} + +static void sd_operation_failed(const char *message) { + sd_throw(SCR_ERR_ERROR, "ERR_CRYPTO_OPERATION_FAILED", message); +} + +/* An OpenSSL DH error as ThrowCryptoError reports it: the formatted + * message plus own library, reason and code properties, in Node's key + * order. */ +static void sd_throw_openssl(const char *packed, const char *reason, const char *code) { + char message[96]; + snprintf(message, sizeof message, "error:%s:Diffie-Hellman routines::%s", packed, reason); + ScrStr *text = scr_str_new(message, strlen(message)); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + ScrStr *library = scr_str_new("Diffie-Hellman routines", 23); + scr_error_set_prop(e, "library", 7, scr_dyn_new_str(library)); + scr_str_release(library); + 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); + scr_error_set_code(e, code); + ScrStr *code_text = scr_str_new(code, strlen(code)); + scr_error_set_prop(e, "code", 4, scr_dyn_new_str(code_text)); + scr_str_release(code_text); + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + +static void sd_bad_generator(void) { + sd_throw_openssl("02800065", "bad generator", "ERR_OSSL_DH_BAD_GENERATOR"); +} + +static void sd_too_long(void) { + static const char message[] = "DiffieHellman primes longer than 8192 bits have no native implementation"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); +} + +/* validateInt32(value, name). */ +static bool sd_validate_int32(const char *name, double value) { + bool integer = isfinite(value) && trunc(value) == value; + if (integer && value >= INT32_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. " + "Received %s", + name, received) + : snprintf(text, sizeof text, + "The value of \"%s\" is out of range. It must be an integer. Received %s", name, + received); + scr_throw_error_msg_code(SCR_ERR_RANGE, text, n < 0 ? 0 : (size_t)n, "ERR_OUT_OF_RANGE"); + return false; +} + +/* ── arithmetic ───────────────────────────────────────────────────────── */ + +/* The exponent digit width that keeps every mbedtls_mpi_exp_mod exponent + * within its 8192-bit limit. */ +#define SD_DIGIT_BITS (MBEDTLS_MPI_MAX_BITS - 1) + +/* base^e mod p, as BN_mod_exp_mont_consttime computes it: false for an + * even modulus or one below 3. Exponents past 8192 bits are split into + * SD_DIGIT_BITS-bit digits. */ +static bool sd_exp(mbedtls_mpi *out, const mbedtls_mpi *base, const mbedtls_mpi *e, const mbedtls_mpi *p) { + if (mbedtls_mpi_cmp_int(p, 1) <= 0 || mbedtls_mpi_get_bit(p, 0) == 0) return false; + size_t ebits = mbedtls_mpi_bitlen(e); + if (ebits <= MBEDTLS_MPI_MAX_BITS) return mbedtls_mpi_exp_mod(out, base, e, p, NULL) == 0; + /* base^e = Π (base^(2^(k·w)))^digit_k for the w-bit digits of e. */ + mbedtls_mpi acc, power, digit, term, step; + mbedtls_mpi_init(&acc); + mbedtls_mpi_init(&power); + mbedtls_mpi_init(&digit); + mbedtls_mpi_init(&term); + mbedtls_mpi_init(&step); + int ret = 0; + MBEDTLS_MPI_CHK(mbedtls_mpi_lset(&acc, 1)); + MBEDTLS_MPI_CHK(mbedtls_mpi_copy(&power, base)); + MBEDTLS_MPI_CHK(mbedtls_mpi_lset(&step, 1)); + MBEDTLS_MPI_CHK(mbedtls_mpi_shift_l(&step, SD_DIGIT_BITS)); + for (size_t offset = 0; offset < ebits; offset += SD_DIGIT_BITS) { + MBEDTLS_MPI_CHK(mbedtls_mpi_copy(&digit, e)); + MBEDTLS_MPI_CHK(mbedtls_mpi_shift_r(&digit, offset)); + for (size_t bit = SD_DIGIT_BITS; bit < mbedtls_mpi_bitlen(&digit); bit++) + MBEDTLS_MPI_CHK(mbedtls_mpi_set_bit(&digit, bit, 0)); + MBEDTLS_MPI_CHK(mbedtls_mpi_exp_mod(&term, &power, &digit, p, NULL)); + MBEDTLS_MPI_CHK(mbedtls_mpi_mul_mpi(&acc, &acc, &term)); + MBEDTLS_MPI_CHK(mbedtls_mpi_mod_mpi(&acc, &acc, p)); + if (offset + SD_DIGIT_BITS < ebits) MBEDTLS_MPI_CHK(mbedtls_mpi_exp_mod(&power, &power, &step, p, NULL)); + } + MBEDTLS_MPI_CHK(mbedtls_mpi_copy(out, &acc)); +cleanup: + mbedtls_mpi_free(&acc); + mbedtls_mpi_free(&power); + mbedtls_mpi_free(&digit); + mbedtls_mpi_free(&term); + mbedtls_mpi_free(&step); + return ret == 0; +} + +/* A uniformly random number of exactly `bits` bits (the top bit set), and + * odd when `odd`, as BN_priv_rand writes it. */ +static bool sd_random_bits(mbedtls_mpi *x, size_t bits, bool odd) { + size_t len = (bits + 7) / 8; + unsigned char *buf = malloc(len); + if (!buf) scr_trap("scriptc: out of memory\n"); + arc4random_buf(buf, len); + unsigned extra = (unsigned)(len * 8 - bits); + buf[0] &= (unsigned char)(0xff >> extra); + buf[0] |= (unsigned char)(0x80 >> extra); + if (odd) buf[len - 1] |= 1; + bool ok = mbedtls_mpi_read_binary(x, buf, len) == 0; + mbedtls_platform_zeroize(buf, len); + free(buf); + return ok; +} + +/* ── primes ───────────────────────────────────────────────────────────── */ + +/* The odd primes below 17864 (OpenSSL's first 2048 primes but 2), which + * trial division divides by. Consecutive primes are grouped into products + * below the half-limb bound mbedtls_mpi_mod_int divides correctly by, so + * one call reduces by several. */ +#define SD_SMALL_PRIMES 2047 +#define SD_SMALL_LIMIT 17864 +#define SD_PRODUCT_LIMIT ((uint64_t)1 << (sizeof(mbedtls_mpi_uint) * 4)) + +typedef struct { + uint16_t primes[SD_SMALL_PRIMES]; + uint64_t products[SD_SMALL_PRIMES]; + uint16_t starts[SD_SMALL_PRIMES + 1]; /* products[g] covers primes[starts[g]] to primes[starts[g + 1] - 1] */ + size_t groups; + uint32_t mods[SD_SMALL_PRIMES]; +} ScDhSmallPrimes; + +static ScDhSmallPrimes *sd_small_primes(void) { + ScDhSmallPrimes *t = calloc(1, sizeof *t); + unsigned char *composite = calloc(SD_SMALL_LIMIT, 1); + if (!t || !composite) scr_trap("scriptc: out of memory\n"); + size_t count = 0; + for (uint32_t n = 3; n < SD_SMALL_LIMIT && count < SD_SMALL_PRIMES; n += 2) { + if (composite[n]) continue; + t->primes[count++] = (uint16_t)n; + for (uint32_t m = n * n; m < SD_SMALL_LIMIT; m += 2 * n) composite[m] = 1; + } + free(composite); + uint64_t product = 1; + for (size_t i = 0; i < SD_SMALL_PRIMES; i++) { + if (product > SD_PRODUCT_LIMIT / t->primes[i]) { + t->products[t->groups++] = product; + t->starts[t->groups] = (uint16_t)i; + product = 1; + } + product *= t->primes[i]; + } + t->products[t->groups++] = product; + t->starts[t->groups] = SD_SMALL_PRIMES; + return t; +} + +/* x mod each small prime, into t->mods. */ +static bool sd_small_residues(ScDhSmallPrimes *t, const mbedtls_mpi *x) { + for (size_t g = 0; g < t->groups; g++) { + mbedtls_mpi_uint r; + if (mbedtls_mpi_mod_int(&r, x, (mbedtls_mpi_sint)t->products[g]) != 0) return false; + for (size_t i = t->starts[g]; i < t->starts[g + 1]; i++) t->mods[i] = (uint32_t)(r % t->primes[i]); + } + return true; +} + +#if defined(__SIZEOF_INT128__) +#define SD_MONT 1 +typedef unsigned __int128 sd_u128; + +/* Montgomery arithmetic modulo an odd n of k 64-bit limbs, little-endian, + * with R = 2^(64k), for the Miller-Rabin rounds. It runs in variable time + * and reads only public values. */ +typedef struct { + size_t k; + uint64_t mm; /* -n^-1 mod 2^64 */ + uint64_t *n; /* k limbs */ + uint64_t *rr; /* R^2 mod n, k limbs */ + uint64_t *acc; /* k limbs */ + uint64_t *table; /* 16·k limbs: base^i·R mod n */ + uint64_t *t; /* 2k + 1 limbs of products */ +} ScDhMont; + +static void sd_limbs_read(uint64_t *out, size_t k, const mbedtls_mpi *x, unsigned char *buf) { + if (mbedtls_mpi_write_binary_le(x, buf, 8 * k) != 0) scr_trap("scriptc: internal error: limb overflow\n"); + for (size_t i = 0; i < k; i++) { + uint64_t w = 0; + for (size_t b = 8; b-- > 0;) w = (w << 8) | buf[8 * i + b]; + out[i] = w; + } +} + +static bool sd_limbs_write(mbedtls_mpi *x, const uint64_t *in, size_t k, unsigned char *buf) { + for (size_t i = 0; i < k; i++) + for (size_t b = 0; b < 8; b++) buf[8 * i + b] = (unsigned char)(in[i] >> (8 * b)); + return mbedtls_mpi_read_binary_le(x, buf, 8 * k) == 0; +} + +static bool sd_mont_init(ScDhMont *m, const mbedtls_mpi *n) { + size_t k = (mbedtls_mpi_bitlen(n) + 63) / 64; + m->k = k; + m->n = calloc(21 * k + 1, sizeof(uint64_t)); + if (!m->n) scr_trap("scriptc: out of memory\n"); + m->rr = m->n + k; + m->acc = m->rr + k; + m->table = m->acc + k; + m->t = m->table + 16 * k; + unsigned char *buf = malloc(8 * k); + if (!buf) scr_trap("scriptc: out of memory\n"); + sd_limbs_read(m->n, k, n, buf); + /* Newton's iteration doubles the correct low bits of n^-1 from 3. */ + uint64_t inverse = m->n[0]; + for (int i = 0; i < 6; i++) inverse *= 2 - m->n[0] * inverse; + m->mm = (uint64_t)0 - inverse; + mbedtls_mpi rr; + mbedtls_mpi_init(&rr); + bool ok = mbedtls_mpi_lset(&rr, 1) == 0 && mbedtls_mpi_shift_l(&rr, 128 * k) == 0 && + mbedtls_mpi_mod_mpi(&rr, &rr, n) == 0; + if (ok) sd_limbs_read(m->rr, k, &rr, buf); + mbedtls_mpi_free(&rr); + free(buf); + return ok; +} + +static void sd_mont_free(ScDhMont *m) { + free(m->n); +} + +/* r = v mod n for v < 2n, given as k limbs and a top limb. */ +static void sd_mont_final(const ScDhMont *m, uint64_t *r, const uint64_t *v, uint64_t top) { + size_t k = m->k; + bool ge = top != 0; + if (!ge) { + ge = true; + for (size_t j = k; j-- > 0;) + if (v[j] != m->n[j]) { + ge = v[j] > m->n[j]; + break; + } + } + if (!ge) { + if (r != v) memmove(r, v, k * sizeof *r); + return; + } + uint64_t borrow = 0; + for (size_t j = 0; j < k; j++) { + sd_u128 d = (sd_u128)v[j] - m->n[j] - borrow; + r[j] = (uint64_t)d; + borrow = (uint64_t)(d >> 64) & 1; + } +} + +/* r = t·R^-1 mod n for the 2k-limb product in t. */ +static void sd_mont_redc(ScDhMont *m, uint64_t *r) { + size_t k = m->k; + uint64_t *t = m->t; + t[2 * k] = 0; + for (size_t i = 0; i < k; i++) { + uint64_t q = t[i] * m->mm; + uint64_t carry = 0; + for (size_t j = 0; j < k; j++) { + sd_u128 v = (sd_u128)q * m->n[j] + t[i + j] + carry; + t[i + j] = (uint64_t)v; + carry = (uint64_t)(v >> 64); + } + for (size_t j = i + k; carry != 0 && j <= 2 * k; j++) { + sd_u128 v = (sd_u128)t[j] + carry; + t[j] = (uint64_t)v; + carry = (uint64_t)(v >> 64); + } + } + sd_mont_final(m, r, t + k, t[2 * k]); +} + +static void sd_mont_mul(ScDhMont *m, uint64_t *r, const uint64_t *a, const uint64_t *b) { + size_t k = m->k; + uint64_t *t = m->t; + memset(t, 0, 2 * k * sizeof *t); + for (size_t i = 0; i < k; i++) { + uint64_t carry = 0; + for (size_t j = 0; j < k; j++) { + sd_u128 v = (sd_u128)a[i] * b[j] + t[i + j] + carry; + t[i + j] = (uint64_t)v; + carry = (uint64_t)(v >> 64); + } + t[i + k] = carry; + } + sd_mont_redc(m, r); +} + +/* r = a^2·R^-1 mod n: the cross products once, doubled, plus the squares. */ +static void sd_mont_sqr(ScDhMont *m, uint64_t *r, const uint64_t *a) { + size_t k = m->k; + uint64_t *t = m->t; + memset(t, 0, 2 * k * sizeof *t); + for (size_t i = 0; i < k; i++) { + uint64_t carry = 0; + for (size_t j = i + 1; j < k; j++) { + sd_u128 v = (sd_u128)a[i] * a[j] + t[i + j] + carry; + t[i + j] = (uint64_t)v; + carry = (uint64_t)(v >> 64); + } + t[i + k] = carry; + } + uint64_t shifted = 0; + for (size_t i = 0; i < 2 * k; i++) { + uint64_t v = t[i]; + t[i] = (v << 1) | shifted; + shifted = v >> 63; + } + uint64_t carry = 0; + for (size_t i = 0; i < k; i++) { + sd_u128 square = (sd_u128)a[i] * a[i]; + sd_u128 low = (sd_u128)t[2 * i] + (uint64_t)square + carry; + t[2 * i] = (uint64_t)low; + sd_u128 high = (sd_u128)t[2 * i + 1] + (uint64_t)(square >> 64) + (uint64_t)(low >> 64); + t[2 * i + 1] = (uint64_t)high; + carry = (uint64_t)(high >> 64); + } + sd_mont_redc(m, r); +} + +/* x = 2x mod n. */ +static void sd_mont_double(const ScDhMont *m, uint64_t *x) { + size_t k = m->k; + uint64_t top = x[k - 1] >> 63; + for (size_t j = k; j-- > 1;) x[j] = (x[j] << 1) | (x[j - 1] >> 63); + x[0] <<= 1; + sd_mont_final(m, x, x, top); +} + +/* out = base^e mod n, or 2^e mod n for a NULL base. A base of 2 doubles + * instead of multiplying; others use a 4-bit fixed window. */ +static bool sd_mont_pow(ScDhMont *m, mbedtls_mpi *out, const mbedtls_mpi *base, const mbedtls_mpi *e) { + size_t k = m->k; + unsigned char *buf = malloc(8 * k); + if (!buf) scr_trap("scriptc: out of memory\n"); + /* acc = R mod n, the Montgomery form of 1. */ + memset(m->t, 0, 2 * k * sizeof *m->t); + memcpy(m->t, m->rr, k * sizeof *m->t); + sd_mont_redc(m, m->acc); + size_t bits = mbedtls_mpi_bitlen(e); + if (!base) { + for (size_t i = bits; i-- > 0;) { + sd_mont_sqr(m, m->acc, m->acc); + if (mbedtls_mpi_get_bit(e, i)) sd_mont_double(m, m->acc); + } + } else { + memcpy(m->table, m->acc, k * sizeof *m->table); + sd_limbs_read(m->table + k, k, base, buf); + sd_mont_mul(m, m->table + k, m->table + k, m->rr); + for (size_t i = 2; i < 16; i++) sd_mont_mul(m, m->table + i * k, m->table + (i - 1) * k, m->table + k); + size_t windows = (bits + 3) / 4; + for (size_t w = windows; w-- > 0;) { + if (w + 1 < windows) + for (int s = 0; s < 4; s++) sd_mont_sqr(m, m->acc, m->acc); + unsigned digit = 0; + for (size_t b = 4; b-- > 0;) digit = (digit << 1) | (unsigned)mbedtls_mpi_get_bit(e, 4 * w + b); + if (digit) sd_mont_mul(m, m->acc, m->acc, m->table + digit * k); + } + } + /* Leave the Montgomery form. */ + memset(m->t, 0, 2 * k * sizeof *m->t); + memcpy(m->t, m->acc, k * sizeof *m->t); + sd_mont_redc(m, m->acc); + bool ok = sd_limbs_write(out, m->acc, k, buf); + free(buf); + return ok; +} +#else +#define SD_MONT 0 +#endif + +/* Miller-Rabin state for an odd n > 3: n - 1 = d·2^s. */ +typedef struct { + mbedtls_mpi n1, d, a, x; + size_t s; +#if SD_MONT + ScDhMont mont; +#else + mbedtls_mpi rr; /* the Montgomery constant mbedtls_mpi_exp_mod caches */ +#endif +} ScDhMillerRabin; + +static bool sd_mr_init(ScDhMillerRabin *m, const mbedtls_mpi *n) { + mbedtls_mpi_init(&m->n1); + mbedtls_mpi_init(&m->d); + mbedtls_mpi_init(&m->a); + mbedtls_mpi_init(&m->x); +#if SD_MONT + if (!sd_mont_init(&m->mont, n)) return false; +#else + mbedtls_mpi_init(&m->rr); +#endif + if (mbedtls_mpi_sub_int(&m->n1, n, 1) != 0 || mbedtls_mpi_copy(&m->d, &m->n1) != 0) return false; + m->s = mbedtls_mpi_lsb(&m->d); + return mbedtls_mpi_shift_r(&m->d, m->s) == 0; +} + +static void sd_mr_free(ScDhMillerRabin *m) { + mbedtls_mpi_free(&m->n1); + mbedtls_mpi_free(&m->d); + mbedtls_mpi_free(&m->a); + mbedtls_mpi_free(&m->x); +#if SD_MONT + sd_mont_free(&m->mont); +#else + mbedtls_mpi_free(&m->rr); +#endif +} + +/* x = a^e mod n, or 2^e mod n when base2. */ +static bool sd_mr_pow(ScDhMillerRabin *m, const mbedtls_mpi *n, const mbedtls_mpi *e, bool base2) { +#if SD_MONT + (void)n; + return sd_mont_pow(&m->mont, &m->x, base2 ? NULL : &m->a, e); +#else + if (base2 && mbedtls_mpi_lset(&m->a, 2) != 0) return false; + return mbedtls_mpi_exp_mod(&m->x, &m->a, e, n, &m->rr) == 0; +#endif +} + +/* One round with the base 2, or with a uniformly random base in + * [2, n - 2]: 1 when n is a strong probable prime to the base, 0 when it + * is composite, -1 when an operation fails. */ +static int sd_mr_round(ScDhMillerRabin *m, const mbedtls_mpi *n, bool base2) { + if (!base2 && mbedtls_mpi_random(&m->a, 2, &m->n1, sd_random, NULL) != 0) return -1; + if (!sd_mr_pow(m, n, &m->d, base2)) return -1; + if (mbedtls_mpi_cmp_int(&m->x, 1) == 0 || mbedtls_mpi_cmp_mpi(&m->x, &m->n1) == 0) return 1; + for (size_t i = 1; i < m->s; i++) { + if (mbedtls_mpi_mul_mpi(&m->x, &m->x, &m->x) != 0 || mbedtls_mpi_mod_mpi(&m->x, &m->x, n) != 0) return -1; + if (mbedtls_mpi_cmp_mpi(&m->x, &m->n1) == 0) return 1; + if (mbedtls_mpi_cmp_int(&m->x, 1) == 0) return 0; + } + return 0; +} + +/* 2^(n-1) ≡ 1 (mod n) for an odd n > 3. */ +static int sd_fermat2(const mbedtls_mpi *n) { + ScDhMillerRabin m; + int ret = sd_mr_init(&m, n) && sd_mr_pow(&m, n, &m.n1, true) ? mbedtls_mpi_cmp_int(&m.x, 1) == 0 : -1; + sd_mr_free(&m); + return ret; +} + +/* OpenSSL's Miller-Rabin round count: 64, or 128 past 2048 bits. */ +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) { + 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; + for (size_t i = 0; i < SD_SMALL_PRIMES; i++) + if (t->mods[i] == 0) return mbedtls_mpi_cmp_int(x, t->primes[i]) == 0; + /* No prime factor up to sqrt(x). */ + if (mbedtls_mpi_cmp_int(x, (mbedtls_mpi_sint)SD_SMALL_LIMIT * SD_SMALL_LIMIT) < 0) return true; + ScDhMillerRabin m; + bool prime = sd_mr_init(&m, x); + int rounds = sd_mr_rounds(mbedtls_mpi_bitlen(x)); + for (int i = 0; i < rounds && prime; i++) prime = sd_mr_round(&m, x, false) == 1; + sd_mr_free(&m); + return prime; +} + +/* 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) { + int codes = 0; + if (mbedtls_mpi_get_bit(p, 0) == 0) codes |= SD_P_NOT_PRIME; + if (mbedtls_mpi_cmp_int(g, 1) <= 0) codes |= SD_NOT_SUITABLE_GENERATOR; + if (mbedtls_mpi_bitlen(p) < SD_MIN_MODULUS_BITS) codes |= SD_MODULUS_TOO_SMALL; + mbedtls_mpi t; + mbedtls_mpi_init(&t); + if (mbedtls_mpi_sub_int(&t, p, 1) != 0) scr_trap("scriptc: out of memory\n"); + if (mbedtls_mpi_cmp_mpi(g, &t) >= 0) codes |= SD_NOT_SUITABLE_GENERATOR; + /* BN_check_prime(p), then BN_check_prime((p - 1) / 2) for a prime p. + * For an odd p > 3 whose (p - 1) / 2 is prime, p is prime exactly when + * 3 does not divide it and 2^(p-1) ≡ 1 (mod p) (Pocklington's + * criterion), so (p - 1) / 2 is tested first. */ + ScDhSmallPrimes *small = sd_small_primes(); + if (mbedtls_mpi_copy(&t, p) != 0 || mbedtls_mpi_shift_r(&t, 1) != 0) scr_trap("scriptc: out of memory\n"); + bool p_prime, q_prime; + if (mbedtls_mpi_get_bit(p, 0) == 0 || mbedtls_mpi_cmp_int(p, 3) <= 0) { + p_prime = sd_is_prime(small, p); + q_prime = p_prime && sd_is_prime(small, &t); + } else { + q_prime = sd_is_prime(small, &t); + mbedtls_mpi_uint r = 0; + if (!q_prime) p_prime = sd_is_prime(small, p); + else p_prime = mbedtls_mpi_mod_int(&r, p, 3) == 0 && r != 0 && sd_fermat2(p) == 1; + } + if (!p_prime) codes |= SD_P_NOT_PRIME; + else if (!q_prime) codes |= SD_P_NOT_SAFE_PRIME; + free(small); + mbedtls_mpi_free(&t); + return codes; +} + +/* The named group whose p and g equal these, as ossl_dh_cache_named_group + * finds it, or NULL. */ +static const ScDhNamed *sd_find_named(const mbedtls_mpi *p, const mbedtls_mpi *g) { + size_t p_len = mbedtls_mpi_size(p); + const ScDhNamed *found = NULL; + mbedtls_mpi x; + mbedtls_mpi_init(&x); + for (size_t i = 0; i < sizeof sd_named / sizeof sd_named[0] && !found; i++) { + const ScDhNamed *n = &sd_named[i]; + if (n->p_len != p_len) continue; + if (mbedtls_mpi_read_binary(&x, n->p, n->p_len) != 0 || mbedtls_mpi_cmp_mpi(&x, p) != 0) continue; + if (mbedtls_mpi_read_binary(&x, n->g, n->g_len) == 0 && mbedtls_mpi_cmp_mpi(&x, g) == 0) found = n; + } + mbedtls_mpi_free(&x); + return found; +} + +/* The safe-prime sieve: the odd primes below a bound that grows with the + * prime's length and do not divide the step `add`, each with add^-1 and a + * start's residue modulo it, and the marks of a window of candidates + * start + k·add. */ +#define SD_SIEVE_WINDOW 65536 + +static uint32_t sd_sieve_bound(size_t bits) { + /* mbedtls_mpi_mod_int divides correctly by values below 2^(limb bits / 2). */ + if (sizeof(mbedtls_mpi_uint) < 8) return 1u << 16; + return bits < 1024 ? 1u << 18 : bits < 2048 ? 1u << 22 : 1u << 24; +} + +typedef struct { + uint32_t *primes; + uint32_t *inverse; + uint32_t *residue; + size_t count; + unsigned char marks[SD_SIEVE_WINDOW]; +} ScDhSieve; + +/* a^-1 mod m for gcd(a, m) = 1. */ +static uint32_t sd_inverse(uint32_t a, uint32_t m) { + int64_t t = 0, next = 1; + int64_t r = m, rest = a; + while (rest != 0) { + int64_t quotient = r / rest; + int64_t t_next = t - quotient * next; + t = next; + next = t_next; + int64_t r_next = r - quotient * rest; + r = rest; + rest = r_next; + } + return (uint32_t)(t < 0 ? t + m : t); +} + +static ScDhSieve *sd_sieve_new(size_t bits, unsigned add) { + uint32_t bound = sd_sieve_bound(bits); + ScDhSieve *sv = calloc(1, sizeof *sv); + unsigned char *composite = calloc(bound / 2, 1); /* composite[i] for 2i + 1 */ + if (!sv || !composite) scr_trap("scriptc: out of memory\n"); + size_t count = 0; + for (uint32_t i = 1; i < bound / 2; i++) { + if (composite[i]) continue; + uint32_t n = 2 * i + 1; + for (uint64_t m = (uint64_t)n * n; m < bound; m += 2 * n) composite[m / 2] = 1; + if (add % n != 0) count++; + } + sv->primes = malloc(count * sizeof(uint32_t)); + sv->inverse = malloc(count * sizeof(uint32_t)); + sv->residue = malloc(count * sizeof(uint32_t)); + if (!sv->primes || !sv->inverse || !sv->residue) scr_trap("scriptc: out of memory\n"); + for (uint32_t i = 1; i < bound / 2; i++) { + uint32_t n = 2 * i + 1; + if (composite[i] || add % n == 0) continue; + sv->inverse[sv->count] = sd_inverse(add % n, n); + sv->primes[sv->count++] = n; + } + free(composite); + return sv; +} + +static void sd_sieve_free(ScDhSieve *sv) { + free(sv->primes); + free(sv->inverse); + free(sv->residue); + free(sv); +} + +/* Marks the candidates start + k·add of the window that a sieve prime + * divides, or whose (p - 1) / 2 it divides, then moves the residues to + * the next window. */ +static void sd_sieve_window(ScDhSieve *sv, unsigned add) { + memset(sv->marks, 0, sizeof sv->marks); + for (size_t i = 0; i < sv->count; i++) { + uint64_t r = sv->primes[i]; + uint64_t s = sv->residue[i]; + /* start + k·add ≡ t (mod r) for t = 0 and t = 1. */ + for (uint64_t t = 0; t < 2; t++) { + uint64_t k = (r + t - s) % r * sv->inverse[i] % r; + for (; k < SD_SIEVE_WINDOW; k += r) sv->marks[k] = 1; + } + sv->residue[i] = (uint32_t)((s + (uint64_t)SD_SIEVE_WINDOW * add) % r); + } +} + +/* A random safe prime of exactly `bits` bits with p ≡ rem (mod add), + * rem ≡ 2 (mod 3), as BN_generate_prime_ex2(p, bits, safe = 1, add, rem) + * draws one: a random start of `bits` bits moved to p ≡ rem (mod add), + * then candidates stepping by `add` past values that a sieve prime + * divides or that make (p - 1) / 2 divisible by one. A candidate is kept + * 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) { + ScDhSieve *sv = sd_sieve_new(bits, add); + int rounds = sd_mr_rounds(bits); + mbedtls_mpi start, q; + mbedtls_mpi_init(&start); + mbedtls_mpi_init(&q); + 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 || + 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)) { + ret = -1; + break; + } + for (size_t i = 0; i < sv->count && ret == 0; i++) { + if (mbedtls_mpi_mod_int(&r, &start, sv->primes[i]) != 0) ret = -1; + sv->residue[i] = (uint32_t)r; + } + bool fits = true; + while (ret == 0 && fits) { + sd_sieve_window(sv, add); + for (uint32_t k = 0; k < SD_SIEVE_WINDOW && ret == 0 && fits; k++) { + if (sv->marks[k]) continue; + if (mbedtls_mpi_add_int(p, &start, (mbedtls_mpi_sint)k * add) != 0 || mbedtls_mpi_copy(&q, p) != 0 || + mbedtls_mpi_shift_r(&q, 1) != 0) { + ret = -1; + break; + } + fits = mbedtls_mpi_bitlen(p) == bits; + if (!fits) break; + ScDhMillerRabin mr; + ret = sd_mr_init(&mr, p) ? sd_mr_round(&mr, p, true) : -1; + sd_mr_free(&mr); + if (ret != 1) continue; + ret = sd_mr_init(&mr, &q) ? sd_mr_round(&mr, &q, true) : -1; + for (int i = 1; i < rounds && ret == 1; i++) ret = sd_mr_round(&mr, &q, false); + sd_mr_free(&mr); + } + if (ret == 0 && fits && mbedtls_mpi_add_int(&start, &start, (mbedtls_mpi_sint)SD_SIEVE_WINDOW * add) != 0) + ret = -1; + } + } + sd_sieve_free(sv); + mbedtls_mpi_free(&start); + mbedtls_mpi_free(&q); + return ret == 1; +} + +/* ── construction ─────────────────────────────────────────────────────── */ + +static ScDh *sd_alloc(void) { + ScDh *d = calloc(1, sizeof *d); + if (!d) scr_trap("scriptc: out of memory\n"); + d->rc = 1; + mbedtls_mpi_init(&d->p); + mbedtls_mpi_init(&d->g); + mbedtls_mpi_init(&d->pub); + mbedtls_mpi_init(&d->priv); + return d; +} + +static ScrDyn *sd_box(ScDh *d) { + ScrDynHandleTag tag = d->group ? SCR_DYNH_DIFFIE_HELLMAN_GROUP : SCR_DYNH_DIFFIE_HELLMAN; + scr_dyn_handle_install(tag, d->group ? &sd_group_ops : &sd_ops); + ScrDyn *result = scr_dyn_new_handle(d, tag); /* retains */ + sd_release(d); + return result; +} + +static bool sd_is_view(const ScrDyn *value) { + return value->kind == SCR_DYN_BYTES || scr_buffer_storage_is(value); +} + +/* Buffer.isEncoding(value). */ +static bool sd_is_encoding(const ScrDyn *value) { + return value->kind == SCR_DYN_STR && value->v.str->len > 0 && ca_normalize_encoding(value->v.str) != NULL; +} + +/* new DiffieHellman(primeLength, generator): generated parameters. */ +static ScrDyn *sd_generate(double bits, const ScrBytes *generator_bytes, double generator) { + if (bits < 2) { + sd_throw_openssl("0280007E", "modulus too small", "ERR_OSSL_DH_MODULUS_TOO_SMALL"); + return NULL; + } + if (generator_bytes) { + sd_throw(SCR_ERR_TYPE, "ERR_INVALID_ARG_TYPE", "Second argument must be an int32"); + return NULL; + } + if (generator < 2) { + sd_bad_generator(); + return NULL; + } + if (bits < SD_MIN_MODULUS_BITS || bits > SD_MAX_MODULUS_BITS) { + sd_throw(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "Invalid DH parameters"); + return NULL; + } + if (bits > MBEDTLS_MPI_MAX_BITS) { + sd_too_long(); + return NULL; + } + unsigned add = 12; + 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) { + sd_release(d); + sd_throw(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "Invalid DH parameters"); + return NULL; + } + /* A safe prime of at least 512 bits with a generator below p - 1 has no + * DH_check findings. */ + d->verify_error = 0; + return sd_box(d); +} + +/* new DiffieHellman(prime, generator) over explicit parameters. */ +static ScrDyn *sd_from_prime(const ScrBytes *prime, const ScrBytes *generator_bytes, double generator) { + ScDh *d = sd_alloc(); + if (mbedtls_mpi_read_binary(&d->p, prime->data, prime->len) != 0) scr_trap("scriptc: out of memory\n"); + if (generator_bytes) { + if (mbedtls_mpi_read_binary(&d->g, generator_bytes->data, generator_bytes->len) != 0) + scr_trap("scriptc: out of memory\n"); + } else if (mbedtls_mpi_lset(&d->g, (mbedtls_mpi_sint)generator) != 0) { + scr_trap("scriptc: out of memory\n"); + } + if (mbedtls_mpi_cmp_int(&d->g, 2) < 0 || + (mbedtls_mpi_bitlen(&d->p) >= SD_MIN_MODULUS_BITS && mbedtls_mpi_cmp_mpi(&d->g, &d->p) >= 0)) { + sd_release(d); + sd_bad_generator(); + return NULL; + } + if (mbedtls_mpi_bitlen(&d->p) > MBEDTLS_MPI_MAX_BITS) { + sd_release(d); + sd_too_long(); + return NULL; + } + d->named = sd_find_named(&d->p, &d->g); + /* A named group's parameters have no DH_check findings. Other + * parameters are checked when verifyError is first read. */ + d->verify_error = d->named ? 0 : -1; + return sd_box(d); +} + +/* new DiffieHellman(sizeOrKey, keyEncoding, generator, genEncoding). */ +static ScrDyn *sd_new(const ScrDyn *size_or_key, const ScrDyn *key_encoding, const ScrDyn *generator, + const ScrDyn *generator_encoding) { + static const char *const types = + "one of type number or string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView"; + bool size = size_or_key->kind == SCR_DYN_NUM; + if (!size && size_or_key->kind != SCR_DYN_STR && !sd_is_view(size_or_key)) { + scr_dyn_arg_type_fail("sizeOrKey", types, size_or_key); + return NULL; + } + if (size && !sd_validate_int32("sizeOrKey", size_or_key->v.num)) return NULL; + /* A truthy keyEncoding that names no encoding is the generator. */ + const ScrDyn *gen = generator; + const ScrDyn *gen_encoding = generator_encoding; + const ScrDyn *encoding = key_encoding; + if (scr_dyn_truthy(key_encoding) && !sd_is_encoding(key_encoding) && + !(key_encoding->kind == SCR_DYN_STR && ca_str_is(key_encoding->v.str, "buffer"))) { + gen_encoding = generator; + gen = key_encoding; + encoding = scr_dyn_undefined(); + } + ScrBytes *prime = NULL; + if (size_or_key->kind == SCR_DYN_STR) prime = ca_string_bytes(SD_SUBJECT, size_or_key->v.str, encoding); + else if (!size) prime = scr_bytes_buffer_source(size_or_key); + if (!size && !prime) return NULL; + ScrBytes *gen_bytes = NULL; + double gen_number = 2; + ScrDyn *result = NULL; + if (!scr_dyn_truthy(gen)) { + gen_number = 2; + } else if (gen->kind == SCR_DYN_NUM) { + if (!sd_validate_int32("generator", gen->v.num)) goto done; + gen_number = gen->v.num; + } else if (gen->kind == SCR_DYN_STR) { + if (!(gen_bytes = ca_string_bytes(SD_SUBJECT, gen->v.str, gen_encoding))) goto done; + } else if (sd_is_view(gen)) { + gen_bytes = scr_bytes_buffer_source(gen); + } else { + scr_dyn_arg_type_fail("generator", types, gen); + goto done; + } + result = size ? sd_generate(size_or_key->v.num + 0, gen_bytes, gen_number) + : sd_from_prime(prime, gen_bytes, gen_number); +done: + if (prime) scr_bytes_release(prime); + if (gen_bytes) scr_bytes_release(gen_bytes); + return result; +} + +ScrDyn *scr_crypto_dh_new(ScrDyn *size_or_key, ScrDyn *key_encoding, ScrDyn *generator, ScrDyn *generator_encoding) { + ScrDyn *args[4] = {size_or_key, key_encoding, generator, generator_encoding}; + ScrDyn *unboxed[4] = {NULL, NULL, NULL, NULL}; + ScrDyn *result = NULL; + for (size_t i = 0; i < 4; i++) + if (!(unboxed[i] = ca_unbox(args[i]))) goto done; + result = sd_new(unboxed[0], unboxed[1], unboxed[2], unboxed[3]); +done: + for (size_t i = 0; i < 4; i++) scr_dyn_release(unboxed[i]); + return result; +} + +/* new DiffieHellmanGroup(name): names match case-insensitively. */ +ScrDyn *scr_crypto_dh_group_new(ScrDyn *name) { + ScrDyn *unboxed = ca_unbox(name); + if (!unboxed) return NULL; + if (unboxed->kind != SCR_DYN_STR) { + scr_dyn_release(unboxed); + sd_throw(SCR_ERR_TYPE, "ERR_INVALID_ARG_TYPE", "Group name must be a string"); + return NULL; + } + const ScDhGroup *group = NULL; + for (size_t i = 0; i < sizeof sd_groups / sizeof sd_groups[0] && !group; i++) + if (ca_ascii_eq_ci(unboxed->v.str->data, unboxed->v.str->len, sd_groups[i].name)) group = &sd_groups[i]; + scr_dyn_release(unboxed); + if (!group) { + sd_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNKNOWN_DH_GROUP", "Unknown DH group"); + return NULL; + } + ScDh *d = sd_alloc(); + d->group = group; + if (mbedtls_mpi_read_binary(&d->p, group->p, group->p_len) != 0 || mbedtls_mpi_lset(&d->g, 2) != 0) + scr_trap("scriptc: out of memory\n"); + d->verify_error = 0; + return sd_box(d); +} + +/* ── methods ──────────────────────────────────────────────────────────── */ + +static ScrDyn *sd_arg(ScrDyn *const *args, size_t argc, size_t index) { + return index < argc ? args[index] : scr_dyn_undefined(); +} + +/* The minimal big-endian bytes of a value, as BignumPointer::Encode + * writes them (none for 0), passed through encode(). */ +static ScrDyn *sd_encode_value(const mbedtls_mpi *value, const ScrDyn *encoding) { + size_t len = mbedtls_mpi_size(value); + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)len); + if (mbedtls_mpi_write_binary(value, out->data, len) != 0) scr_trap("scriptc: out of memory\n"); + return ca_encode(SD_SUBJECT, out, encoding); +} + +/* A new private key for generateKeys(). */ +static bool sd_new_private_key(ScDh *d, mbedtls_mpi *priv) { + if (d->group) { + if (d->group->private_bits) return sd_random_bits(priv, (size_t)d->group->private_bits, false); + /* BN_priv_rand_range(p - 3) + 2. */ + mbedtls_mpi bound; + mbedtls_mpi_init(&bound); + bool ok = mbedtls_mpi_sub_int(&bound, &d->p, 1) == 0 && mbedtls_mpi_random(priv, 2, &bound, sd_random, NULL) == 0; + mbedtls_mpi_free(&bound); + return ok; + } + size_t bits = mbedtls_mpi_bitlen(&d->p); + if (bits < SD_MIN_MODULUS_BITS || bits > SD_MAX_MODULUS_BITS) return false; + if (!d->named) return sd_random_bits(priv, bits - 2, false); + /* ossl_ffc_generate_private_key: 1 <= priv < min(2^N, q). */ + mbedtls_mpi bound, q; + mbedtls_mpi_init(&bound); + mbedtls_mpi_init(&q); + bool ok = mbedtls_mpi_lset(&bound, 1) == 0 && mbedtls_mpi_shift_l(&bound, (size_t)d->named->keylength) == 0; + if (ok && d->named->q) ok = mbedtls_mpi_read_binary(&q, d->named->q, d->named->q_len) == 0; + else if (ok) ok = mbedtls_mpi_copy(&q, &d->p) == 0 && mbedtls_mpi_shift_r(&q, 1) == 0; + if (ok && mbedtls_mpi_cmp_mpi(&q, &bound) < 0) ok = mbedtls_mpi_copy(&bound, &q) == 0; + ok = ok && mbedtls_mpi_random(priv, 1, &bound, sd_random, NULL) == 0; + mbedtls_mpi_free(&bound); + mbedtls_mpi_free(&q); + return ok; +} + +/* generateKeys(encoding): a new key pair, or the public key of the + * private key the object has. */ +static ScrDyn *sd_generate_keys(ScDh *d, const ScrDyn *encoding) { + mbedtls_mpi priv, pub; + mbedtls_mpi_init(&priv); + mbedtls_mpi_init(&pub); + bool fresh = !d->has_priv; + bool ok = fresh ? sd_new_private_key(d, &priv) : mbedtls_mpi_copy(&priv, &d->priv) == 0; + ok = ok && sd_exp(&pub, &d->g, &priv, &d->p); + if (ok) { + mbedtls_mpi_swap(&d->priv, &priv); + mbedtls_mpi_swap(&d->pub, &pub); + d->has_priv = true; + d->has_pub = true; + } + mbedtls_mpi_free(&priv); + mbedtls_mpi_free(&pub); + if (!ok || mbedtls_mpi_cmp_int(&d->pub, 0) == 0) { + sd_operation_failed("Key generation failed"); + return NULL; + } + return sd_encode_value(&d->pub, encoding); +} + +/* Whether OpenSSL's DH derivation accepts explicit parameters' private + * key: a nonzero key and a 512- to 10000-bit prime. */ +static bool sd_derives(const ScDh *d) { + size_t bits = mbedtls_mpi_bitlen(&d->p); + return mbedtls_mpi_cmp_int(&d->priv, 0) != 0 && bits >= SD_MIN_MODULUS_BITS && bits <= SD_MAX_MODULUS_BITS; +} + +/* computeSecret(key, inputEncoding, outputEncoding): key^priv mod p, + * padded to the prime's length. */ +static ScrDyn *sd_compute_secret(ScDh *d, const ScrDyn *key, const ScrDyn *in_enc, const ScrDyn *out_enc) { + ScrBytes *bytes = ca_key_bytes(SD_SUBJECT, key, in_enc); + if (!bytes) return NULL; + ScrDyn *result = NULL; + mbedtls_mpi peer, bound, secret; + mbedtls_mpi_init(&peer); + mbedtls_mpi_init(&bound); + mbedtls_mpi_init(&secret); + if (mbedtls_mpi_read_binary(&peer, bytes->data, bytes->len) != 0 || mbedtls_mpi_sub_int(&bound, &d->p, 1) != 0) + scr_trap("scriptc: out of memory\n"); + if (mbedtls_mpi_cmp_int(&peer, 1) <= 0) { + sd_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYLEN", "Supplied key is too small"); + } else if (mbedtls_mpi_cmp_mpi(&peer, &bound) >= 0) { + sd_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYLEN", "Supplied key is too large"); + } else if (!d->has_priv || (!d->group && !sd_derives(d)) || !sd_exp(&secret, &peer, &d->priv, &d->p)) { + sd_operation_failed("Failed to compute shared secret"); + } else { + size_t len = mbedtls_mpi_size(&d->p); + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)len); + if (mbedtls_mpi_write_binary(&secret, out->data, len) != 0) scr_trap("scriptc: out of memory\n"); + result = ca_encode(SD_SUBJECT, out, out_enc); + } + mbedtls_mpi_free(&peer); + mbedtls_mpi_free(&bound); + mbedtls_mpi_free(&secret); + scr_bytes_release(bytes); + return result; +} + +static ScrDyn *sd_get_value(const mbedtls_mpi *value, bool present, const char *missing, const ScrDyn *encoding) { + if (!present || mbedtls_mpi_cmp_int(value, 0) == 0) { + sd_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INVALID_STATE", missing); + return NULL; + } + return sd_encode_value(value, encoding); +} + +/* setPublicKey(key, encoding) and setPrivateKey(key, encoding). */ +static ScrDyn *sd_set_value(mbedtls_mpi *value, bool *present, ScrDyn *self, const ScrDyn *key, const ScrDyn *encoding) { + ScrBytes *bytes = ca_key_bytes(SD_SUBJECT, key, encoding); + if (!bytes) return NULL; + if (mbedtls_mpi_read_binary(value, bytes->data, bytes->len) != 0) scr_trap("scriptc: out of memory\n"); + *present = true; + scr_bytes_release(bytes); + return scr_dyn_retain(self); +} + +static bool sd_name_is(const char *method, const char *name) { + return strcmp(method, name) == 0; +} + +static ScrDyn *sd_dispatch(ScDh *d, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + const ScrDyn *a0 = sd_arg(args, argc, 0); + if (sd_name_is(method, "generateKeys")) return sd_generate_keys(d, a0); + if (sd_name_is(method, "computeSecret")) + return sd_compute_secret(d, a0, sd_arg(args, argc, 1), sd_arg(args, argc, 2)); + if (sd_name_is(method, "getPrime")) return sd_get_value(&d->p, true, "p is null", a0); + if (sd_name_is(method, "getGenerator")) return sd_get_value(&d->g, true, "g is null", a0); + if (sd_name_is(method, "getPublicKey")) + return sd_get_value(&d->pub, d->has_pub, "No public key - did you forget to generate one?", a0); + if (sd_name_is(method, "getPrivateKey")) + return sd_get_value(&d->priv, d->has_priv, "No private key - did you forget to generate one?", a0); + /* DiffieHellmanGroup has no key setters. */ + if (!d->group && sd_name_is(method, "setPublicKey")) + return sd_set_value(&d->pub, &d->has_pub, self, a0, sd_arg(args, argc, 1)); + if (!d->group && sd_name_is(method, "setPrivateKey")) + return sd_set_value(&d->priv, &d->has_priv, self, a0, sd_arg(args, argc, 1)); + /* The Object.prototype methods a DiffieHellman inherits. */ + static const char *const inherited[] = {"constructor", "hasOwnProperty", "isPrototypeOf", + "propertyIsEnumerable", "toLocaleString", "toString", + "valueOf"}; + for (size_t i = 0; i < sizeof inherited / sizeof inherited[0]; i++) + if (sd_name_is(method, inherited[i])) { + static const char message[] = "DiffieHellman's Object.prototype methods have no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; + } + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, what); + scr_jb_puts(&b, " is not a function"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return NULL; +} + +/* Every method reads at most three arguments. */ +static ScrDyn *sd_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + ScrDyn *unboxed[3] = {NULL, NULL, NULL}; + size_t count = argc < 3 ? argc : 3; + ScrDyn *result = NULL; + for (size_t i = 0; i < count; i++) + if (!(unboxed[i] = ca_unbox(args[i]))) goto done; + result = sd_dispatch(h, self, method, unboxed, count, what); +done: + for (size_t i = 0; i < count; i++) scr_dyn_release(unboxed[i]); + return result; +} + +/* verifyError, the one own data property. */ +static ScrDyn *sd_get(void *h, const char *key, size_t key_len) { + ScDh *d = h; + if (key_len == 11 && memcmp(key, "verifyError", 11) == 0) { + if (d->verify_error < 0) d->verify_error = sd_check_params(&d->p, &d->g); + return scr_dyn_new_num(d->verify_error); + } + static const char message[] = + "DiffieHellman property reads other than verifyError have no lowering; call generateKeys(), " + "computeSecret(), getPrime(), getGenerator(), getPublicKey(), getPrivateKey(), setPublicKey() or " + "setPrivateKey()"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; +} + +static bool sd_set(void *h, const char *key, size_t key_len, const ScrDyn *value) { + (void)h; (void)key; (void)key_len; (void)value; + static const char message[] = "Writing DiffieHellman properties has no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return true; +} + +static ScrDynHandleOps sd_ops = {"DiffieHellman", sd_retain, sd_release, sd_invoke, sd_get, sd_set, NULL, NULL, NULL}; +static ScrDynHandleOps sd_group_ops = {"DiffieHellmanGroup", sd_retain, sd_release, sd_invoke, sd_get, sd_set, + NULL, NULL, NULL}; diff --git a/packages/runtime/src/scr_dh_groups.h b/packages/runtime/src/scr_dh_groups.h new file mode 100644 index 0000000000..c82ec72703 --- /dev/null +++ b/packages/runtime/src/scr_dh_groups.h @@ -0,0 +1,557 @@ +/* Diffie-Hellman group parameters, big-endian: the primes behind Node 24's + * crypto.getDiffieHellman groups (modp1 and modp2 from RFC 2409, modp5 and + * modp14 through modp18 from RFC 3526), and the RFC 7919 (ffdhe2048 through + * ffdhe8192) and RFC 5114 (dh_1024_160, dh_2048_224 and dh_2048_256) + * parameters OpenSSL 3.5 lists among its DH named groups + * (crypto/ffc/ffc_dh.c). Every group but RFC 5114's uses the generator 2 + * and a safe prime. */ +#ifndef SCR_DH_GROUPS_H +#define SCR_DH_GROUPS_H + +static const unsigned char scr_dh_modp1_p[96] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_modp2_p[128] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, + 0x49, 0x28, 0x66, 0x51, 0xec, 0xe6, 0x53, 0x81, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_modp5_p[192] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, + 0x49, 0x28, 0x66, 0x51, 0xec, 0xe4, 0x5b, 0x3d, 0xc2, 0x00, 0x7c, 0xb8, 0xa1, 0x63, 0xbf, 0x05, + 0x98, 0xda, 0x48, 0x36, 0x1c, 0x55, 0xd3, 0x9a, 0x69, 0x16, 0x3f, 0xa8, 0xfd, 0x24, 0xcf, 0x5f, + 0x83, 0x65, 0x5d, 0x23, 0xdc, 0xa3, 0xad, 0x96, 0x1c, 0x62, 0xf3, 0x56, 0x20, 0x85, 0x52, 0xbb, + 0x9e, 0xd5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6d, 0x67, 0x0c, 0x35, 0x4e, 0x4a, 0xbc, 0x98, 0x04, + 0xf1, 0x74, 0x6c, 0x08, 0xca, 0x23, 0x73, 0x27, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_modp14_p[256] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, + 0x49, 0x28, 0x66, 0x51, 0xec, 0xe4, 0x5b, 0x3d, 0xc2, 0x00, 0x7c, 0xb8, 0xa1, 0x63, 0xbf, 0x05, + 0x98, 0xda, 0x48, 0x36, 0x1c, 0x55, 0xd3, 0x9a, 0x69, 0x16, 0x3f, 0xa8, 0xfd, 0x24, 0xcf, 0x5f, + 0x83, 0x65, 0x5d, 0x23, 0xdc, 0xa3, 0xad, 0x96, 0x1c, 0x62, 0xf3, 0x56, 0x20, 0x85, 0x52, 0xbb, + 0x9e, 0xd5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6d, 0x67, 0x0c, 0x35, 0x4e, 0x4a, 0xbc, 0x98, 0x04, + 0xf1, 0x74, 0x6c, 0x08, 0xca, 0x18, 0x21, 0x7c, 0x32, 0x90, 0x5e, 0x46, 0x2e, 0x36, 0xce, 0x3b, + 0xe3, 0x9e, 0x77, 0x2c, 0x18, 0x0e, 0x86, 0x03, 0x9b, 0x27, 0x83, 0xa2, 0xec, 0x07, 0xa2, 0x8f, + 0xb5, 0xc5, 0x5d, 0xf0, 0x6f, 0x4c, 0x52, 0xc9, 0xde, 0x2b, 0xcb, 0xf6, 0x95, 0x58, 0x17, 0x18, + 0x39, 0x95, 0x49, 0x7c, 0xea, 0x95, 0x6a, 0xe5, 0x15, 0xd2, 0x26, 0x18, 0x98, 0xfa, 0x05, 0x10, + 0x15, 0x72, 0x8e, 0x5a, 0x8a, 0xac, 0xaa, 0x68, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_modp15_p[384] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, + 0x49, 0x28, 0x66, 0x51, 0xec, 0xe4, 0x5b, 0x3d, 0xc2, 0x00, 0x7c, 0xb8, 0xa1, 0x63, 0xbf, 0x05, + 0x98, 0xda, 0x48, 0x36, 0x1c, 0x55, 0xd3, 0x9a, 0x69, 0x16, 0x3f, 0xa8, 0xfd, 0x24, 0xcf, 0x5f, + 0x83, 0x65, 0x5d, 0x23, 0xdc, 0xa3, 0xad, 0x96, 0x1c, 0x62, 0xf3, 0x56, 0x20, 0x85, 0x52, 0xbb, + 0x9e, 0xd5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6d, 0x67, 0x0c, 0x35, 0x4e, 0x4a, 0xbc, 0x98, 0x04, + 0xf1, 0x74, 0x6c, 0x08, 0xca, 0x18, 0x21, 0x7c, 0x32, 0x90, 0x5e, 0x46, 0x2e, 0x36, 0xce, 0x3b, + 0xe3, 0x9e, 0x77, 0x2c, 0x18, 0x0e, 0x86, 0x03, 0x9b, 0x27, 0x83, 0xa2, 0xec, 0x07, 0xa2, 0x8f, + 0xb5, 0xc5, 0x5d, 0xf0, 0x6f, 0x4c, 0x52, 0xc9, 0xde, 0x2b, 0xcb, 0xf6, 0x95, 0x58, 0x17, 0x18, + 0x39, 0x95, 0x49, 0x7c, 0xea, 0x95, 0x6a, 0xe5, 0x15, 0xd2, 0x26, 0x18, 0x98, 0xfa, 0x05, 0x10, + 0x15, 0x72, 0x8e, 0x5a, 0x8a, 0xaa, 0xc4, 0x2d, 0xad, 0x33, 0x17, 0x0d, 0x04, 0x50, 0x7a, 0x33, + 0xa8, 0x55, 0x21, 0xab, 0xdf, 0x1c, 0xba, 0x64, 0xec, 0xfb, 0x85, 0x04, 0x58, 0xdb, 0xef, 0x0a, + 0x8a, 0xea, 0x71, 0x57, 0x5d, 0x06, 0x0c, 0x7d, 0xb3, 0x97, 0x0f, 0x85, 0xa6, 0xe1, 0xe4, 0xc7, + 0xab, 0xf5, 0xae, 0x8c, 0xdb, 0x09, 0x33, 0xd7, 0x1e, 0x8c, 0x94, 0xe0, 0x4a, 0x25, 0x61, 0x9d, + 0xce, 0xe3, 0xd2, 0x26, 0x1a, 0xd2, 0xee, 0x6b, 0xf1, 0x2f, 0xfa, 0x06, 0xd9, 0x8a, 0x08, 0x64, + 0xd8, 0x76, 0x02, 0x73, 0x3e, 0xc8, 0x6a, 0x64, 0x52, 0x1f, 0x2b, 0x18, 0x17, 0x7b, 0x20, 0x0c, + 0xbb, 0xe1, 0x17, 0x57, 0x7a, 0x61, 0x5d, 0x6c, 0x77, 0x09, 0x88, 0xc0, 0xba, 0xd9, 0x46, 0xe2, + 0x08, 0xe2, 0x4f, 0xa0, 0x74, 0xe5, 0xab, 0x31, 0x43, 0xdb, 0x5b, 0xfc, 0xe0, 0xfd, 0x10, 0x8e, + 0x4b, 0x82, 0xd1, 0x20, 0xa9, 0x3a, 0xd2, 0xca, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_modp16_p[512] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, + 0x49, 0x28, 0x66, 0x51, 0xec, 0xe4, 0x5b, 0x3d, 0xc2, 0x00, 0x7c, 0xb8, 0xa1, 0x63, 0xbf, 0x05, + 0x98, 0xda, 0x48, 0x36, 0x1c, 0x55, 0xd3, 0x9a, 0x69, 0x16, 0x3f, 0xa8, 0xfd, 0x24, 0xcf, 0x5f, + 0x83, 0x65, 0x5d, 0x23, 0xdc, 0xa3, 0xad, 0x96, 0x1c, 0x62, 0xf3, 0x56, 0x20, 0x85, 0x52, 0xbb, + 0x9e, 0xd5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6d, 0x67, 0x0c, 0x35, 0x4e, 0x4a, 0xbc, 0x98, 0x04, + 0xf1, 0x74, 0x6c, 0x08, 0xca, 0x18, 0x21, 0x7c, 0x32, 0x90, 0x5e, 0x46, 0x2e, 0x36, 0xce, 0x3b, + 0xe3, 0x9e, 0x77, 0x2c, 0x18, 0x0e, 0x86, 0x03, 0x9b, 0x27, 0x83, 0xa2, 0xec, 0x07, 0xa2, 0x8f, + 0xb5, 0xc5, 0x5d, 0xf0, 0x6f, 0x4c, 0x52, 0xc9, 0xde, 0x2b, 0xcb, 0xf6, 0x95, 0x58, 0x17, 0x18, + 0x39, 0x95, 0x49, 0x7c, 0xea, 0x95, 0x6a, 0xe5, 0x15, 0xd2, 0x26, 0x18, 0x98, 0xfa, 0x05, 0x10, + 0x15, 0x72, 0x8e, 0x5a, 0x8a, 0xaa, 0xc4, 0x2d, 0xad, 0x33, 0x17, 0x0d, 0x04, 0x50, 0x7a, 0x33, + 0xa8, 0x55, 0x21, 0xab, 0xdf, 0x1c, 0xba, 0x64, 0xec, 0xfb, 0x85, 0x04, 0x58, 0xdb, 0xef, 0x0a, + 0x8a, 0xea, 0x71, 0x57, 0x5d, 0x06, 0x0c, 0x7d, 0xb3, 0x97, 0x0f, 0x85, 0xa6, 0xe1, 0xe4, 0xc7, + 0xab, 0xf5, 0xae, 0x8c, 0xdb, 0x09, 0x33, 0xd7, 0x1e, 0x8c, 0x94, 0xe0, 0x4a, 0x25, 0x61, 0x9d, + 0xce, 0xe3, 0xd2, 0x26, 0x1a, 0xd2, 0xee, 0x6b, 0xf1, 0x2f, 0xfa, 0x06, 0xd9, 0x8a, 0x08, 0x64, + 0xd8, 0x76, 0x02, 0x73, 0x3e, 0xc8, 0x6a, 0x64, 0x52, 0x1f, 0x2b, 0x18, 0x17, 0x7b, 0x20, 0x0c, + 0xbb, 0xe1, 0x17, 0x57, 0x7a, 0x61, 0x5d, 0x6c, 0x77, 0x09, 0x88, 0xc0, 0xba, 0xd9, 0x46, 0xe2, + 0x08, 0xe2, 0x4f, 0xa0, 0x74, 0xe5, 0xab, 0x31, 0x43, 0xdb, 0x5b, 0xfc, 0xe0, 0xfd, 0x10, 0x8e, + 0x4b, 0x82, 0xd1, 0x20, 0xa9, 0x21, 0x08, 0x01, 0x1a, 0x72, 0x3c, 0x12, 0xa7, 0x87, 0xe6, 0xd7, + 0x88, 0x71, 0x9a, 0x10, 0xbd, 0xba, 0x5b, 0x26, 0x99, 0xc3, 0x27, 0x18, 0x6a, 0xf4, 0xe2, 0x3c, + 0x1a, 0x94, 0x68, 0x34, 0xb6, 0x15, 0x0b, 0xda, 0x25, 0x83, 0xe9, 0xca, 0x2a, 0xd4, 0x4c, 0xe8, + 0xdb, 0xbb, 0xc2, 0xdb, 0x04, 0xde, 0x8e, 0xf9, 0x2e, 0x8e, 0xfc, 0x14, 0x1f, 0xbe, 0xca, 0xa6, + 0x28, 0x7c, 0x59, 0x47, 0x4e, 0x6b, 0xc0, 0x5d, 0x99, 0xb2, 0x96, 0x4f, 0xa0, 0x90, 0xc3, 0xa2, + 0x23, 0x3b, 0xa1, 0x86, 0x51, 0x5b, 0xe7, 0xed, 0x1f, 0x61, 0x29, 0x70, 0xce, 0xe2, 0xd7, 0xaf, + 0xb8, 0x1b, 0xdd, 0x76, 0x21, 0x70, 0x48, 0x1c, 0xd0, 0x06, 0x91, 0x27, 0xd5, 0xb0, 0x5a, 0xa9, + 0x93, 0xb4, 0xea, 0x98, 0x8d, 0x8f, 0xdd, 0xc1, 0x86, 0xff, 0xb7, 0xdc, 0x90, 0xa6, 0xc0, 0x8f, + 0x4d, 0xf4, 0x35, 0xc9, 0x34, 0x06, 0x31, 0x99, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_modp17_p[768] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, + 0x49, 0x28, 0x66, 0x51, 0xec, 0xe4, 0x5b, 0x3d, 0xc2, 0x00, 0x7c, 0xb8, 0xa1, 0x63, 0xbf, 0x05, + 0x98, 0xda, 0x48, 0x36, 0x1c, 0x55, 0xd3, 0x9a, 0x69, 0x16, 0x3f, 0xa8, 0xfd, 0x24, 0xcf, 0x5f, + 0x83, 0x65, 0x5d, 0x23, 0xdc, 0xa3, 0xad, 0x96, 0x1c, 0x62, 0xf3, 0x56, 0x20, 0x85, 0x52, 0xbb, + 0x9e, 0xd5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6d, 0x67, 0x0c, 0x35, 0x4e, 0x4a, 0xbc, 0x98, 0x04, + 0xf1, 0x74, 0x6c, 0x08, 0xca, 0x18, 0x21, 0x7c, 0x32, 0x90, 0x5e, 0x46, 0x2e, 0x36, 0xce, 0x3b, + 0xe3, 0x9e, 0x77, 0x2c, 0x18, 0x0e, 0x86, 0x03, 0x9b, 0x27, 0x83, 0xa2, 0xec, 0x07, 0xa2, 0x8f, + 0xb5, 0xc5, 0x5d, 0xf0, 0x6f, 0x4c, 0x52, 0xc9, 0xde, 0x2b, 0xcb, 0xf6, 0x95, 0x58, 0x17, 0x18, + 0x39, 0x95, 0x49, 0x7c, 0xea, 0x95, 0x6a, 0xe5, 0x15, 0xd2, 0x26, 0x18, 0x98, 0xfa, 0x05, 0x10, + 0x15, 0x72, 0x8e, 0x5a, 0x8a, 0xaa, 0xc4, 0x2d, 0xad, 0x33, 0x17, 0x0d, 0x04, 0x50, 0x7a, 0x33, + 0xa8, 0x55, 0x21, 0xab, 0xdf, 0x1c, 0xba, 0x64, 0xec, 0xfb, 0x85, 0x04, 0x58, 0xdb, 0xef, 0x0a, + 0x8a, 0xea, 0x71, 0x57, 0x5d, 0x06, 0x0c, 0x7d, 0xb3, 0x97, 0x0f, 0x85, 0xa6, 0xe1, 0xe4, 0xc7, + 0xab, 0xf5, 0xae, 0x8c, 0xdb, 0x09, 0x33, 0xd7, 0x1e, 0x8c, 0x94, 0xe0, 0x4a, 0x25, 0x61, 0x9d, + 0xce, 0xe3, 0xd2, 0x26, 0x1a, 0xd2, 0xee, 0x6b, 0xf1, 0x2f, 0xfa, 0x06, 0xd9, 0x8a, 0x08, 0x64, + 0xd8, 0x76, 0x02, 0x73, 0x3e, 0xc8, 0x6a, 0x64, 0x52, 0x1f, 0x2b, 0x18, 0x17, 0x7b, 0x20, 0x0c, + 0xbb, 0xe1, 0x17, 0x57, 0x7a, 0x61, 0x5d, 0x6c, 0x77, 0x09, 0x88, 0xc0, 0xba, 0xd9, 0x46, 0xe2, + 0x08, 0xe2, 0x4f, 0xa0, 0x74, 0xe5, 0xab, 0x31, 0x43, 0xdb, 0x5b, 0xfc, 0xe0, 0xfd, 0x10, 0x8e, + 0x4b, 0x82, 0xd1, 0x20, 0xa9, 0x21, 0x08, 0x01, 0x1a, 0x72, 0x3c, 0x12, 0xa7, 0x87, 0xe6, 0xd7, + 0x88, 0x71, 0x9a, 0x10, 0xbd, 0xba, 0x5b, 0x26, 0x99, 0xc3, 0x27, 0x18, 0x6a, 0xf4, 0xe2, 0x3c, + 0x1a, 0x94, 0x68, 0x34, 0xb6, 0x15, 0x0b, 0xda, 0x25, 0x83, 0xe9, 0xca, 0x2a, 0xd4, 0x4c, 0xe8, + 0xdb, 0xbb, 0xc2, 0xdb, 0x04, 0xde, 0x8e, 0xf9, 0x2e, 0x8e, 0xfc, 0x14, 0x1f, 0xbe, 0xca, 0xa6, + 0x28, 0x7c, 0x59, 0x47, 0x4e, 0x6b, 0xc0, 0x5d, 0x99, 0xb2, 0x96, 0x4f, 0xa0, 0x90, 0xc3, 0xa2, + 0x23, 0x3b, 0xa1, 0x86, 0x51, 0x5b, 0xe7, 0xed, 0x1f, 0x61, 0x29, 0x70, 0xce, 0xe2, 0xd7, 0xaf, + 0xb8, 0x1b, 0xdd, 0x76, 0x21, 0x70, 0x48, 0x1c, 0xd0, 0x06, 0x91, 0x27, 0xd5, 0xb0, 0x5a, 0xa9, + 0x93, 0xb4, 0xea, 0x98, 0x8d, 0x8f, 0xdd, 0xc1, 0x86, 0xff, 0xb7, 0xdc, 0x90, 0xa6, 0xc0, 0x8f, + 0x4d, 0xf4, 0x35, 0xc9, 0x34, 0x02, 0x84, 0x92, 0x36, 0xc3, 0xfa, 0xb4, 0xd2, 0x7c, 0x70, 0x26, + 0xc1, 0xd4, 0xdc, 0xb2, 0x60, 0x26, 0x46, 0xde, 0xc9, 0x75, 0x1e, 0x76, 0x3d, 0xba, 0x37, 0xbd, + 0xf8, 0xff, 0x94, 0x06, 0xad, 0x9e, 0x53, 0x0e, 0xe5, 0xdb, 0x38, 0x2f, 0x41, 0x30, 0x01, 0xae, + 0xb0, 0x6a, 0x53, 0xed, 0x90, 0x27, 0xd8, 0x31, 0x17, 0x97, 0x27, 0xb0, 0x86, 0x5a, 0x89, 0x18, + 0xda, 0x3e, 0xdb, 0xeb, 0xcf, 0x9b, 0x14, 0xed, 0x44, 0xce, 0x6c, 0xba, 0xce, 0xd4, 0xbb, 0x1b, + 0xdb, 0x7f, 0x14, 0x47, 0xe6, 0xcc, 0x25, 0x4b, 0x33, 0x20, 0x51, 0x51, 0x2b, 0xd7, 0xaf, 0x42, + 0x6f, 0xb8, 0xf4, 0x01, 0x37, 0x8c, 0xd2, 0xbf, 0x59, 0x83, 0xca, 0x01, 0xc6, 0x4b, 0x92, 0xec, + 0xf0, 0x32, 0xea, 0x15, 0xd1, 0x72, 0x1d, 0x03, 0xf4, 0x82, 0xd7, 0xce, 0x6e, 0x74, 0xfe, 0xf6, + 0xd5, 0x5e, 0x70, 0x2f, 0x46, 0x98, 0x0c, 0x82, 0xb5, 0xa8, 0x40, 0x31, 0x90, 0x0b, 0x1c, 0x9e, + 0x59, 0xe7, 0xc9, 0x7f, 0xbe, 0xc7, 0xe8, 0xf3, 0x23, 0xa9, 0x7a, 0x7e, 0x36, 0xcc, 0x88, 0xbe, + 0x0f, 0x1d, 0x45, 0xb7, 0xff, 0x58, 0x5a, 0xc5, 0x4b, 0xd4, 0x07, 0xb2, 0x2b, 0x41, 0x54, 0xaa, + 0xcc, 0x8f, 0x6d, 0x7e, 0xbf, 0x48, 0xe1, 0xd8, 0x14, 0xcc, 0x5e, 0xd2, 0x0f, 0x80, 0x37, 0xe0, + 0xa7, 0x97, 0x15, 0xee, 0xf2, 0x9b, 0xe3, 0x28, 0x06, 0xa1, 0xd5, 0x8b, 0xb7, 0xc5, 0xda, 0x76, + 0xf5, 0x50, 0xaa, 0x3d, 0x8a, 0x1f, 0xbf, 0xf0, 0xeb, 0x19, 0xcc, 0xb1, 0xa3, 0x13, 0xd5, 0x5c, + 0xda, 0x56, 0xc9, 0xec, 0x2e, 0xf2, 0x96, 0x32, 0x38, 0x7f, 0xe8, 0xd7, 0x6e, 0x3c, 0x04, 0x68, + 0x04, 0x3e, 0x8f, 0x66, 0x3f, 0x48, 0x60, 0xee, 0x12, 0xbf, 0x2d, 0x5b, 0x0b, 0x74, 0x74, 0xd6, + 0xe6, 0x94, 0xf9, 0x1e, 0x6d, 0xcc, 0x40, 0x24, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_modp18_p[1024] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, + 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74, + 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, + 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37, + 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, + 0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6, + 0x49, 0x28, 0x66, 0x51, 0xec, 0xe4, 0x5b, 0x3d, 0xc2, 0x00, 0x7c, 0xb8, 0xa1, 0x63, 0xbf, 0x05, + 0x98, 0xda, 0x48, 0x36, 0x1c, 0x55, 0xd3, 0x9a, 0x69, 0x16, 0x3f, 0xa8, 0xfd, 0x24, 0xcf, 0x5f, + 0x83, 0x65, 0x5d, 0x23, 0xdc, 0xa3, 0xad, 0x96, 0x1c, 0x62, 0xf3, 0x56, 0x20, 0x85, 0x52, 0xbb, + 0x9e, 0xd5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6d, 0x67, 0x0c, 0x35, 0x4e, 0x4a, 0xbc, 0x98, 0x04, + 0xf1, 0x74, 0x6c, 0x08, 0xca, 0x18, 0x21, 0x7c, 0x32, 0x90, 0x5e, 0x46, 0x2e, 0x36, 0xce, 0x3b, + 0xe3, 0x9e, 0x77, 0x2c, 0x18, 0x0e, 0x86, 0x03, 0x9b, 0x27, 0x83, 0xa2, 0xec, 0x07, 0xa2, 0x8f, + 0xb5, 0xc5, 0x5d, 0xf0, 0x6f, 0x4c, 0x52, 0xc9, 0xde, 0x2b, 0xcb, 0xf6, 0x95, 0x58, 0x17, 0x18, + 0x39, 0x95, 0x49, 0x7c, 0xea, 0x95, 0x6a, 0xe5, 0x15, 0xd2, 0x26, 0x18, 0x98, 0xfa, 0x05, 0x10, + 0x15, 0x72, 0x8e, 0x5a, 0x8a, 0xaa, 0xc4, 0x2d, 0xad, 0x33, 0x17, 0x0d, 0x04, 0x50, 0x7a, 0x33, + 0xa8, 0x55, 0x21, 0xab, 0xdf, 0x1c, 0xba, 0x64, 0xec, 0xfb, 0x85, 0x04, 0x58, 0xdb, 0xef, 0x0a, + 0x8a, 0xea, 0x71, 0x57, 0x5d, 0x06, 0x0c, 0x7d, 0xb3, 0x97, 0x0f, 0x85, 0xa6, 0xe1, 0xe4, 0xc7, + 0xab, 0xf5, 0xae, 0x8c, 0xdb, 0x09, 0x33, 0xd7, 0x1e, 0x8c, 0x94, 0xe0, 0x4a, 0x25, 0x61, 0x9d, + 0xce, 0xe3, 0xd2, 0x26, 0x1a, 0xd2, 0xee, 0x6b, 0xf1, 0x2f, 0xfa, 0x06, 0xd9, 0x8a, 0x08, 0x64, + 0xd8, 0x76, 0x02, 0x73, 0x3e, 0xc8, 0x6a, 0x64, 0x52, 0x1f, 0x2b, 0x18, 0x17, 0x7b, 0x20, 0x0c, + 0xbb, 0xe1, 0x17, 0x57, 0x7a, 0x61, 0x5d, 0x6c, 0x77, 0x09, 0x88, 0xc0, 0xba, 0xd9, 0x46, 0xe2, + 0x08, 0xe2, 0x4f, 0xa0, 0x74, 0xe5, 0xab, 0x31, 0x43, 0xdb, 0x5b, 0xfc, 0xe0, 0xfd, 0x10, 0x8e, + 0x4b, 0x82, 0xd1, 0x20, 0xa9, 0x21, 0x08, 0x01, 0x1a, 0x72, 0x3c, 0x12, 0xa7, 0x87, 0xe6, 0xd7, + 0x88, 0x71, 0x9a, 0x10, 0xbd, 0xba, 0x5b, 0x26, 0x99, 0xc3, 0x27, 0x18, 0x6a, 0xf4, 0xe2, 0x3c, + 0x1a, 0x94, 0x68, 0x34, 0xb6, 0x15, 0x0b, 0xda, 0x25, 0x83, 0xe9, 0xca, 0x2a, 0xd4, 0x4c, 0xe8, + 0xdb, 0xbb, 0xc2, 0xdb, 0x04, 0xde, 0x8e, 0xf9, 0x2e, 0x8e, 0xfc, 0x14, 0x1f, 0xbe, 0xca, 0xa6, + 0x28, 0x7c, 0x59, 0x47, 0x4e, 0x6b, 0xc0, 0x5d, 0x99, 0xb2, 0x96, 0x4f, 0xa0, 0x90, 0xc3, 0xa2, + 0x23, 0x3b, 0xa1, 0x86, 0x51, 0x5b, 0xe7, 0xed, 0x1f, 0x61, 0x29, 0x70, 0xce, 0xe2, 0xd7, 0xaf, + 0xb8, 0x1b, 0xdd, 0x76, 0x21, 0x70, 0x48, 0x1c, 0xd0, 0x06, 0x91, 0x27, 0xd5, 0xb0, 0x5a, 0xa9, + 0x93, 0xb4, 0xea, 0x98, 0x8d, 0x8f, 0xdd, 0xc1, 0x86, 0xff, 0xb7, 0xdc, 0x90, 0xa6, 0xc0, 0x8f, + 0x4d, 0xf4, 0x35, 0xc9, 0x34, 0x02, 0x84, 0x92, 0x36, 0xc3, 0xfa, 0xb4, 0xd2, 0x7c, 0x70, 0x26, + 0xc1, 0xd4, 0xdc, 0xb2, 0x60, 0x26, 0x46, 0xde, 0xc9, 0x75, 0x1e, 0x76, 0x3d, 0xba, 0x37, 0xbd, + 0xf8, 0xff, 0x94, 0x06, 0xad, 0x9e, 0x53, 0x0e, 0xe5, 0xdb, 0x38, 0x2f, 0x41, 0x30, 0x01, 0xae, + 0xb0, 0x6a, 0x53, 0xed, 0x90, 0x27, 0xd8, 0x31, 0x17, 0x97, 0x27, 0xb0, 0x86, 0x5a, 0x89, 0x18, + 0xda, 0x3e, 0xdb, 0xeb, 0xcf, 0x9b, 0x14, 0xed, 0x44, 0xce, 0x6c, 0xba, 0xce, 0xd4, 0xbb, 0x1b, + 0xdb, 0x7f, 0x14, 0x47, 0xe6, 0xcc, 0x25, 0x4b, 0x33, 0x20, 0x51, 0x51, 0x2b, 0xd7, 0xaf, 0x42, + 0x6f, 0xb8, 0xf4, 0x01, 0x37, 0x8c, 0xd2, 0xbf, 0x59, 0x83, 0xca, 0x01, 0xc6, 0x4b, 0x92, 0xec, + 0xf0, 0x32, 0xea, 0x15, 0xd1, 0x72, 0x1d, 0x03, 0xf4, 0x82, 0xd7, 0xce, 0x6e, 0x74, 0xfe, 0xf6, + 0xd5, 0x5e, 0x70, 0x2f, 0x46, 0x98, 0x0c, 0x82, 0xb5, 0xa8, 0x40, 0x31, 0x90, 0x0b, 0x1c, 0x9e, + 0x59, 0xe7, 0xc9, 0x7f, 0xbe, 0xc7, 0xe8, 0xf3, 0x23, 0xa9, 0x7a, 0x7e, 0x36, 0xcc, 0x88, 0xbe, + 0x0f, 0x1d, 0x45, 0xb7, 0xff, 0x58, 0x5a, 0xc5, 0x4b, 0xd4, 0x07, 0xb2, 0x2b, 0x41, 0x54, 0xaa, + 0xcc, 0x8f, 0x6d, 0x7e, 0xbf, 0x48, 0xe1, 0xd8, 0x14, 0xcc, 0x5e, 0xd2, 0x0f, 0x80, 0x37, 0xe0, + 0xa7, 0x97, 0x15, 0xee, 0xf2, 0x9b, 0xe3, 0x28, 0x06, 0xa1, 0xd5, 0x8b, 0xb7, 0xc5, 0xda, 0x76, + 0xf5, 0x50, 0xaa, 0x3d, 0x8a, 0x1f, 0xbf, 0xf0, 0xeb, 0x19, 0xcc, 0xb1, 0xa3, 0x13, 0xd5, 0x5c, + 0xda, 0x56, 0xc9, 0xec, 0x2e, 0xf2, 0x96, 0x32, 0x38, 0x7f, 0xe8, 0xd7, 0x6e, 0x3c, 0x04, 0x68, + 0x04, 0x3e, 0x8f, 0x66, 0x3f, 0x48, 0x60, 0xee, 0x12, 0xbf, 0x2d, 0x5b, 0x0b, 0x74, 0x74, 0xd6, + 0xe6, 0x94, 0xf9, 0x1e, 0x6d, 0xbe, 0x11, 0x59, 0x74, 0xa3, 0x92, 0x6f, 0x12, 0xfe, 0xe5, 0xe4, + 0x38, 0x77, 0x7c, 0xb6, 0xa9, 0x32, 0xdf, 0x8c, 0xd8, 0xbe, 0xc4, 0xd0, 0x73, 0xb9, 0x31, 0xba, + 0x3b, 0xc8, 0x32, 0xb6, 0x8d, 0x9d, 0xd3, 0x00, 0x74, 0x1f, 0xa7, 0xbf, 0x8a, 0xfc, 0x47, 0xed, + 0x25, 0x76, 0xf6, 0x93, 0x6b, 0xa4, 0x24, 0x66, 0x3a, 0xab, 0x63, 0x9c, 0x5a, 0xe4, 0xf5, 0x68, + 0x34, 0x23, 0xb4, 0x74, 0x2b, 0xf1, 0xc9, 0x78, 0x23, 0x8f, 0x16, 0xcb, 0xe3, 0x9d, 0x65, 0x2d, + 0xe3, 0xfd, 0xb8, 0xbe, 0xfc, 0x84, 0x8a, 0xd9, 0x22, 0x22, 0x2e, 0x04, 0xa4, 0x03, 0x7c, 0x07, + 0x13, 0xeb, 0x57, 0xa8, 0x1a, 0x23, 0xf0, 0xc7, 0x34, 0x73, 0xfc, 0x64, 0x6c, 0xea, 0x30, 0x6b, + 0x4b, 0xcb, 0xc8, 0x86, 0x2f, 0x83, 0x85, 0xdd, 0xfa, 0x9d, 0x4b, 0x7f, 0xa2, 0xc0, 0x87, 0xe8, + 0x79, 0x68, 0x33, 0x03, 0xed, 0x5b, 0xdd, 0x3a, 0x06, 0x2b, 0x3c, 0xf5, 0xb3, 0xa2, 0x78, 0xa6, + 0x6d, 0x2a, 0x13, 0xf8, 0x3f, 0x44, 0xf8, 0x2d, 0xdf, 0x31, 0x0e, 0xe0, 0x74, 0xab, 0x6a, 0x36, + 0x45, 0x97, 0xe8, 0x99, 0xa0, 0x25, 0x5d, 0xc1, 0x64, 0xf3, 0x1c, 0xc5, 0x08, 0x46, 0x85, 0x1d, + 0xf9, 0xab, 0x48, 0x19, 0x5d, 0xed, 0x7e, 0xa1, 0xb1, 0xd5, 0x10, 0xbd, 0x7e, 0xe7, 0x4d, 0x73, + 0xfa, 0xf3, 0x6b, 0xc3, 0x1e, 0xcf, 0xa2, 0x68, 0x35, 0x90, 0x46, 0xf4, 0xeb, 0x87, 0x9f, 0x92, + 0x40, 0x09, 0x43, 0x8b, 0x48, 0x1c, 0x6c, 0xd7, 0x88, 0x9a, 0x00, 0x2e, 0xd5, 0xee, 0x38, 0x2b, + 0xc9, 0x19, 0x0d, 0xa6, 0xfc, 0x02, 0x6e, 0x47, 0x95, 0x58, 0xe4, 0x47, 0x56, 0x77, 0xe9, 0xaa, + 0x9e, 0x30, 0x50, 0xe2, 0x76, 0x56, 0x94, 0xdf, 0xc8, 0x1f, 0x56, 0xe8, 0x80, 0xb9, 0x6e, 0x71, + 0x60, 0xc9, 0x80, 0xdd, 0x98, 0xed, 0xd3, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_ffdhe2048_p[256] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xad, 0xf8, 0x54, 0x58, 0xa2, 0xbb, 0x4a, 0x9a, + 0xaf, 0xdc, 0x56, 0x20, 0x27, 0x3d, 0x3c, 0xf1, 0xd8, 0xb9, 0xc5, 0x83, 0xce, 0x2d, 0x36, 0x95, + 0xa9, 0xe1, 0x36, 0x41, 0x14, 0x64, 0x33, 0xfb, 0xcc, 0x93, 0x9d, 0xce, 0x24, 0x9b, 0x3e, 0xf9, + 0x7d, 0x2f, 0xe3, 0x63, 0x63, 0x0c, 0x75, 0xd8, 0xf6, 0x81, 0xb2, 0x02, 0xae, 0xc4, 0x61, 0x7a, + 0xd3, 0xdf, 0x1e, 0xd5, 0xd5, 0xfd, 0x65, 0x61, 0x24, 0x33, 0xf5, 0x1f, 0x5f, 0x06, 0x6e, 0xd0, + 0x85, 0x63, 0x65, 0x55, 0x3d, 0xed, 0x1a, 0xf3, 0xb5, 0x57, 0x13, 0x5e, 0x7f, 0x57, 0xc9, 0x35, + 0x98, 0x4f, 0x0c, 0x70, 0xe0, 0xe6, 0x8b, 0x77, 0xe2, 0xa6, 0x89, 0xda, 0xf3, 0xef, 0xe8, 0x72, + 0x1d, 0xf1, 0x58, 0xa1, 0x36, 0xad, 0xe7, 0x35, 0x30, 0xac, 0xca, 0x4f, 0x48, 0x3a, 0x79, 0x7a, + 0xbc, 0x0a, 0xb1, 0x82, 0xb3, 0x24, 0xfb, 0x61, 0xd1, 0x08, 0xa9, 0x4b, 0xb2, 0xc8, 0xe3, 0xfb, + 0xb9, 0x6a, 0xda, 0xb7, 0x60, 0xd7, 0xf4, 0x68, 0x1d, 0x4f, 0x42, 0xa3, 0xde, 0x39, 0x4d, 0xf4, + 0xae, 0x56, 0xed, 0xe7, 0x63, 0x72, 0xbb, 0x19, 0x0b, 0x07, 0xa7, 0xc8, 0xee, 0x0a, 0x6d, 0x70, + 0x9e, 0x02, 0xfc, 0xe1, 0xcd, 0xf7, 0xe2, 0xec, 0xc0, 0x34, 0x04, 0xcd, 0x28, 0x34, 0x2f, 0x61, + 0x91, 0x72, 0xfe, 0x9c, 0xe9, 0x85, 0x83, 0xff, 0x8e, 0x4f, 0x12, 0x32, 0xee, 0xf2, 0x81, 0x83, + 0xc3, 0xfe, 0x3b, 0x1b, 0x4c, 0x6f, 0xad, 0x73, 0x3b, 0xb5, 0xfc, 0xbc, 0x2e, 0xc2, 0x20, 0x05, + 0xc5, 0x8e, 0xf1, 0x83, 0x7d, 0x16, 0x83, 0xb2, 0xc6, 0xf3, 0x4a, 0x26, 0xc1, 0xb2, 0xef, 0xfa, + 0x88, 0x6b, 0x42, 0x38, 0x61, 0x28, 0x5c, 0x97, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_ffdhe3072_p[384] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xad, 0xf8, 0x54, 0x58, 0xa2, 0xbb, 0x4a, 0x9a, + 0xaf, 0xdc, 0x56, 0x20, 0x27, 0x3d, 0x3c, 0xf1, 0xd8, 0xb9, 0xc5, 0x83, 0xce, 0x2d, 0x36, 0x95, + 0xa9, 0xe1, 0x36, 0x41, 0x14, 0x64, 0x33, 0xfb, 0xcc, 0x93, 0x9d, 0xce, 0x24, 0x9b, 0x3e, 0xf9, + 0x7d, 0x2f, 0xe3, 0x63, 0x63, 0x0c, 0x75, 0xd8, 0xf6, 0x81, 0xb2, 0x02, 0xae, 0xc4, 0x61, 0x7a, + 0xd3, 0xdf, 0x1e, 0xd5, 0xd5, 0xfd, 0x65, 0x61, 0x24, 0x33, 0xf5, 0x1f, 0x5f, 0x06, 0x6e, 0xd0, + 0x85, 0x63, 0x65, 0x55, 0x3d, 0xed, 0x1a, 0xf3, 0xb5, 0x57, 0x13, 0x5e, 0x7f, 0x57, 0xc9, 0x35, + 0x98, 0x4f, 0x0c, 0x70, 0xe0, 0xe6, 0x8b, 0x77, 0xe2, 0xa6, 0x89, 0xda, 0xf3, 0xef, 0xe8, 0x72, + 0x1d, 0xf1, 0x58, 0xa1, 0x36, 0xad, 0xe7, 0x35, 0x30, 0xac, 0xca, 0x4f, 0x48, 0x3a, 0x79, 0x7a, + 0xbc, 0x0a, 0xb1, 0x82, 0xb3, 0x24, 0xfb, 0x61, 0xd1, 0x08, 0xa9, 0x4b, 0xb2, 0xc8, 0xe3, 0xfb, + 0xb9, 0x6a, 0xda, 0xb7, 0x60, 0xd7, 0xf4, 0x68, 0x1d, 0x4f, 0x42, 0xa3, 0xde, 0x39, 0x4d, 0xf4, + 0xae, 0x56, 0xed, 0xe7, 0x63, 0x72, 0xbb, 0x19, 0x0b, 0x07, 0xa7, 0xc8, 0xee, 0x0a, 0x6d, 0x70, + 0x9e, 0x02, 0xfc, 0xe1, 0xcd, 0xf7, 0xe2, 0xec, 0xc0, 0x34, 0x04, 0xcd, 0x28, 0x34, 0x2f, 0x61, + 0x91, 0x72, 0xfe, 0x9c, 0xe9, 0x85, 0x83, 0xff, 0x8e, 0x4f, 0x12, 0x32, 0xee, 0xf2, 0x81, 0x83, + 0xc3, 0xfe, 0x3b, 0x1b, 0x4c, 0x6f, 0xad, 0x73, 0x3b, 0xb5, 0xfc, 0xbc, 0x2e, 0xc2, 0x20, 0x05, + 0xc5, 0x8e, 0xf1, 0x83, 0x7d, 0x16, 0x83, 0xb2, 0xc6, 0xf3, 0x4a, 0x26, 0xc1, 0xb2, 0xef, 0xfa, + 0x88, 0x6b, 0x42, 0x38, 0x61, 0x1f, 0xcf, 0xdc, 0xde, 0x35, 0x5b, 0x3b, 0x65, 0x19, 0x03, 0x5b, + 0xbc, 0x34, 0xf4, 0xde, 0xf9, 0x9c, 0x02, 0x38, 0x61, 0xb4, 0x6f, 0xc9, 0xd6, 0xe6, 0xc9, 0x07, + 0x7a, 0xd9, 0x1d, 0x26, 0x91, 0xf7, 0xf7, 0xee, 0x59, 0x8c, 0xb0, 0xfa, 0xc1, 0x86, 0xd9, 0x1c, + 0xae, 0xfe, 0x13, 0x09, 0x85, 0x13, 0x92, 0x70, 0xb4, 0x13, 0x0c, 0x93, 0xbc, 0x43, 0x79, 0x44, + 0xf4, 0xfd, 0x44, 0x52, 0xe2, 0xd7, 0x4d, 0xd3, 0x64, 0xf2, 0xe2, 0x1e, 0x71, 0xf5, 0x4b, 0xff, + 0x5c, 0xae, 0x82, 0xab, 0x9c, 0x9d, 0xf6, 0x9e, 0xe8, 0x6d, 0x2b, 0xc5, 0x22, 0x36, 0x3a, 0x0d, + 0xab, 0xc5, 0x21, 0x97, 0x9b, 0x0d, 0xea, 0xda, 0x1d, 0xbf, 0x9a, 0x42, 0xd5, 0xc4, 0x48, 0x4e, + 0x0a, 0xbc, 0xd0, 0x6b, 0xfa, 0x53, 0xdd, 0xef, 0x3c, 0x1b, 0x20, 0xee, 0x3f, 0xd5, 0x9d, 0x7c, + 0x25, 0xe4, 0x1d, 0x2b, 0x66, 0xc6, 0x2e, 0x37, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_ffdhe4096_p[512] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xad, 0xf8, 0x54, 0x58, 0xa2, 0xbb, 0x4a, 0x9a, + 0xaf, 0xdc, 0x56, 0x20, 0x27, 0x3d, 0x3c, 0xf1, 0xd8, 0xb9, 0xc5, 0x83, 0xce, 0x2d, 0x36, 0x95, + 0xa9, 0xe1, 0x36, 0x41, 0x14, 0x64, 0x33, 0xfb, 0xcc, 0x93, 0x9d, 0xce, 0x24, 0x9b, 0x3e, 0xf9, + 0x7d, 0x2f, 0xe3, 0x63, 0x63, 0x0c, 0x75, 0xd8, 0xf6, 0x81, 0xb2, 0x02, 0xae, 0xc4, 0x61, 0x7a, + 0xd3, 0xdf, 0x1e, 0xd5, 0xd5, 0xfd, 0x65, 0x61, 0x24, 0x33, 0xf5, 0x1f, 0x5f, 0x06, 0x6e, 0xd0, + 0x85, 0x63, 0x65, 0x55, 0x3d, 0xed, 0x1a, 0xf3, 0xb5, 0x57, 0x13, 0x5e, 0x7f, 0x57, 0xc9, 0x35, + 0x98, 0x4f, 0x0c, 0x70, 0xe0, 0xe6, 0x8b, 0x77, 0xe2, 0xa6, 0x89, 0xda, 0xf3, 0xef, 0xe8, 0x72, + 0x1d, 0xf1, 0x58, 0xa1, 0x36, 0xad, 0xe7, 0x35, 0x30, 0xac, 0xca, 0x4f, 0x48, 0x3a, 0x79, 0x7a, + 0xbc, 0x0a, 0xb1, 0x82, 0xb3, 0x24, 0xfb, 0x61, 0xd1, 0x08, 0xa9, 0x4b, 0xb2, 0xc8, 0xe3, 0xfb, + 0xb9, 0x6a, 0xda, 0xb7, 0x60, 0xd7, 0xf4, 0x68, 0x1d, 0x4f, 0x42, 0xa3, 0xde, 0x39, 0x4d, 0xf4, + 0xae, 0x56, 0xed, 0xe7, 0x63, 0x72, 0xbb, 0x19, 0x0b, 0x07, 0xa7, 0xc8, 0xee, 0x0a, 0x6d, 0x70, + 0x9e, 0x02, 0xfc, 0xe1, 0xcd, 0xf7, 0xe2, 0xec, 0xc0, 0x34, 0x04, 0xcd, 0x28, 0x34, 0x2f, 0x61, + 0x91, 0x72, 0xfe, 0x9c, 0xe9, 0x85, 0x83, 0xff, 0x8e, 0x4f, 0x12, 0x32, 0xee, 0xf2, 0x81, 0x83, + 0xc3, 0xfe, 0x3b, 0x1b, 0x4c, 0x6f, 0xad, 0x73, 0x3b, 0xb5, 0xfc, 0xbc, 0x2e, 0xc2, 0x20, 0x05, + 0xc5, 0x8e, 0xf1, 0x83, 0x7d, 0x16, 0x83, 0xb2, 0xc6, 0xf3, 0x4a, 0x26, 0xc1, 0xb2, 0xef, 0xfa, + 0x88, 0x6b, 0x42, 0x38, 0x61, 0x1f, 0xcf, 0xdc, 0xde, 0x35, 0x5b, 0x3b, 0x65, 0x19, 0x03, 0x5b, + 0xbc, 0x34, 0xf4, 0xde, 0xf9, 0x9c, 0x02, 0x38, 0x61, 0xb4, 0x6f, 0xc9, 0xd6, 0xe6, 0xc9, 0x07, + 0x7a, 0xd9, 0x1d, 0x26, 0x91, 0xf7, 0xf7, 0xee, 0x59, 0x8c, 0xb0, 0xfa, 0xc1, 0x86, 0xd9, 0x1c, + 0xae, 0xfe, 0x13, 0x09, 0x85, 0x13, 0x92, 0x70, 0xb4, 0x13, 0x0c, 0x93, 0xbc, 0x43, 0x79, 0x44, + 0xf4, 0xfd, 0x44, 0x52, 0xe2, 0xd7, 0x4d, 0xd3, 0x64, 0xf2, 0xe2, 0x1e, 0x71, 0xf5, 0x4b, 0xff, + 0x5c, 0xae, 0x82, 0xab, 0x9c, 0x9d, 0xf6, 0x9e, 0xe8, 0x6d, 0x2b, 0xc5, 0x22, 0x36, 0x3a, 0x0d, + 0xab, 0xc5, 0x21, 0x97, 0x9b, 0x0d, 0xea, 0xda, 0x1d, 0xbf, 0x9a, 0x42, 0xd5, 0xc4, 0x48, 0x4e, + 0x0a, 0xbc, 0xd0, 0x6b, 0xfa, 0x53, 0xdd, 0xef, 0x3c, 0x1b, 0x20, 0xee, 0x3f, 0xd5, 0x9d, 0x7c, + 0x25, 0xe4, 0x1d, 0x2b, 0x66, 0x9e, 0x1e, 0xf1, 0x6e, 0x6f, 0x52, 0xc3, 0x16, 0x4d, 0xf4, 0xfb, + 0x79, 0x30, 0xe9, 0xe4, 0xe5, 0x88, 0x57, 0xb6, 0xac, 0x7d, 0x5f, 0x42, 0xd6, 0x9f, 0x6d, 0x18, + 0x77, 0x63, 0xcf, 0x1d, 0x55, 0x03, 0x40, 0x04, 0x87, 0xf5, 0x5b, 0xa5, 0x7e, 0x31, 0xcc, 0x7a, + 0x71, 0x35, 0xc8, 0x86, 0xef, 0xb4, 0x31, 0x8a, 0xed, 0x6a, 0x1e, 0x01, 0x2d, 0x9e, 0x68, 0x32, + 0xa9, 0x07, 0x60, 0x0a, 0x91, 0x81, 0x30, 0xc4, 0x6d, 0xc7, 0x78, 0xf9, 0x71, 0xad, 0x00, 0x38, + 0x09, 0x29, 0x99, 0xa3, 0x33, 0xcb, 0x8b, 0x7a, 0x1a, 0x1d, 0xb9, 0x3d, 0x71, 0x40, 0x00, 0x3c, + 0x2a, 0x4e, 0xce, 0xa9, 0xf9, 0x8d, 0x0a, 0xcc, 0x0a, 0x82, 0x91, 0xcd, 0xce, 0xc9, 0x7d, 0xcf, + 0x8e, 0xc9, 0xb5, 0x5a, 0x7f, 0x88, 0xa4, 0x6b, 0x4d, 0xb5, 0xa8, 0x51, 0xf4, 0x41, 0x82, 0xe1, + 0xc6, 0x8a, 0x00, 0x7e, 0x5e, 0x65, 0x5f, 0x6a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_ffdhe6144_p[768] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xad, 0xf8, 0x54, 0x58, 0xa2, 0xbb, 0x4a, 0x9a, + 0xaf, 0xdc, 0x56, 0x20, 0x27, 0x3d, 0x3c, 0xf1, 0xd8, 0xb9, 0xc5, 0x83, 0xce, 0x2d, 0x36, 0x95, + 0xa9, 0xe1, 0x36, 0x41, 0x14, 0x64, 0x33, 0xfb, 0xcc, 0x93, 0x9d, 0xce, 0x24, 0x9b, 0x3e, 0xf9, + 0x7d, 0x2f, 0xe3, 0x63, 0x63, 0x0c, 0x75, 0xd8, 0xf6, 0x81, 0xb2, 0x02, 0xae, 0xc4, 0x61, 0x7a, + 0xd3, 0xdf, 0x1e, 0xd5, 0xd5, 0xfd, 0x65, 0x61, 0x24, 0x33, 0xf5, 0x1f, 0x5f, 0x06, 0x6e, 0xd0, + 0x85, 0x63, 0x65, 0x55, 0x3d, 0xed, 0x1a, 0xf3, 0xb5, 0x57, 0x13, 0x5e, 0x7f, 0x57, 0xc9, 0x35, + 0x98, 0x4f, 0x0c, 0x70, 0xe0, 0xe6, 0x8b, 0x77, 0xe2, 0xa6, 0x89, 0xda, 0xf3, 0xef, 0xe8, 0x72, + 0x1d, 0xf1, 0x58, 0xa1, 0x36, 0xad, 0xe7, 0x35, 0x30, 0xac, 0xca, 0x4f, 0x48, 0x3a, 0x79, 0x7a, + 0xbc, 0x0a, 0xb1, 0x82, 0xb3, 0x24, 0xfb, 0x61, 0xd1, 0x08, 0xa9, 0x4b, 0xb2, 0xc8, 0xe3, 0xfb, + 0xb9, 0x6a, 0xda, 0xb7, 0x60, 0xd7, 0xf4, 0x68, 0x1d, 0x4f, 0x42, 0xa3, 0xde, 0x39, 0x4d, 0xf4, + 0xae, 0x56, 0xed, 0xe7, 0x63, 0x72, 0xbb, 0x19, 0x0b, 0x07, 0xa7, 0xc8, 0xee, 0x0a, 0x6d, 0x70, + 0x9e, 0x02, 0xfc, 0xe1, 0xcd, 0xf7, 0xe2, 0xec, 0xc0, 0x34, 0x04, 0xcd, 0x28, 0x34, 0x2f, 0x61, + 0x91, 0x72, 0xfe, 0x9c, 0xe9, 0x85, 0x83, 0xff, 0x8e, 0x4f, 0x12, 0x32, 0xee, 0xf2, 0x81, 0x83, + 0xc3, 0xfe, 0x3b, 0x1b, 0x4c, 0x6f, 0xad, 0x73, 0x3b, 0xb5, 0xfc, 0xbc, 0x2e, 0xc2, 0x20, 0x05, + 0xc5, 0x8e, 0xf1, 0x83, 0x7d, 0x16, 0x83, 0xb2, 0xc6, 0xf3, 0x4a, 0x26, 0xc1, 0xb2, 0xef, 0xfa, + 0x88, 0x6b, 0x42, 0x38, 0x61, 0x1f, 0xcf, 0xdc, 0xde, 0x35, 0x5b, 0x3b, 0x65, 0x19, 0x03, 0x5b, + 0xbc, 0x34, 0xf4, 0xde, 0xf9, 0x9c, 0x02, 0x38, 0x61, 0xb4, 0x6f, 0xc9, 0xd6, 0xe6, 0xc9, 0x07, + 0x7a, 0xd9, 0x1d, 0x26, 0x91, 0xf7, 0xf7, 0xee, 0x59, 0x8c, 0xb0, 0xfa, 0xc1, 0x86, 0xd9, 0x1c, + 0xae, 0xfe, 0x13, 0x09, 0x85, 0x13, 0x92, 0x70, 0xb4, 0x13, 0x0c, 0x93, 0xbc, 0x43, 0x79, 0x44, + 0xf4, 0xfd, 0x44, 0x52, 0xe2, 0xd7, 0x4d, 0xd3, 0x64, 0xf2, 0xe2, 0x1e, 0x71, 0xf5, 0x4b, 0xff, + 0x5c, 0xae, 0x82, 0xab, 0x9c, 0x9d, 0xf6, 0x9e, 0xe8, 0x6d, 0x2b, 0xc5, 0x22, 0x36, 0x3a, 0x0d, + 0xab, 0xc5, 0x21, 0x97, 0x9b, 0x0d, 0xea, 0xda, 0x1d, 0xbf, 0x9a, 0x42, 0xd5, 0xc4, 0x48, 0x4e, + 0x0a, 0xbc, 0xd0, 0x6b, 0xfa, 0x53, 0xdd, 0xef, 0x3c, 0x1b, 0x20, 0xee, 0x3f, 0xd5, 0x9d, 0x7c, + 0x25, 0xe4, 0x1d, 0x2b, 0x66, 0x9e, 0x1e, 0xf1, 0x6e, 0x6f, 0x52, 0xc3, 0x16, 0x4d, 0xf4, 0xfb, + 0x79, 0x30, 0xe9, 0xe4, 0xe5, 0x88, 0x57, 0xb6, 0xac, 0x7d, 0x5f, 0x42, 0xd6, 0x9f, 0x6d, 0x18, + 0x77, 0x63, 0xcf, 0x1d, 0x55, 0x03, 0x40, 0x04, 0x87, 0xf5, 0x5b, 0xa5, 0x7e, 0x31, 0xcc, 0x7a, + 0x71, 0x35, 0xc8, 0x86, 0xef, 0xb4, 0x31, 0x8a, 0xed, 0x6a, 0x1e, 0x01, 0x2d, 0x9e, 0x68, 0x32, + 0xa9, 0x07, 0x60, 0x0a, 0x91, 0x81, 0x30, 0xc4, 0x6d, 0xc7, 0x78, 0xf9, 0x71, 0xad, 0x00, 0x38, + 0x09, 0x29, 0x99, 0xa3, 0x33, 0xcb, 0x8b, 0x7a, 0x1a, 0x1d, 0xb9, 0x3d, 0x71, 0x40, 0x00, 0x3c, + 0x2a, 0x4e, 0xce, 0xa9, 0xf9, 0x8d, 0x0a, 0xcc, 0x0a, 0x82, 0x91, 0xcd, 0xce, 0xc9, 0x7d, 0xcf, + 0x8e, 0xc9, 0xb5, 0x5a, 0x7f, 0x88, 0xa4, 0x6b, 0x4d, 0xb5, 0xa8, 0x51, 0xf4, 0x41, 0x82, 0xe1, + 0xc6, 0x8a, 0x00, 0x7e, 0x5e, 0x0d, 0xd9, 0x02, 0x0b, 0xfd, 0x64, 0xb6, 0x45, 0x03, 0x6c, 0x7a, + 0x4e, 0x67, 0x7d, 0x2c, 0x38, 0x53, 0x2a, 0x3a, 0x23, 0xba, 0x44, 0x42, 0xca, 0xf5, 0x3e, 0xa6, + 0x3b, 0xb4, 0x54, 0x32, 0x9b, 0x76, 0x24, 0xc8, 0x91, 0x7b, 0xdd, 0x64, 0xb1, 0xc0, 0xfd, 0x4c, + 0xb3, 0x8e, 0x8c, 0x33, 0x4c, 0x70, 0x1c, 0x3a, 0xcd, 0xad, 0x06, 0x57, 0xfc, 0xcf, 0xec, 0x71, + 0x9b, 0x1f, 0x5c, 0x3e, 0x4e, 0x46, 0x04, 0x1f, 0x38, 0x81, 0x47, 0xfb, 0x4c, 0xfd, 0xb4, 0x77, + 0xa5, 0x24, 0x71, 0xf7, 0xa9, 0xa9, 0x69, 0x10, 0xb8, 0x55, 0x32, 0x2e, 0xdb, 0x63, 0x40, 0xd8, + 0xa0, 0x0e, 0xf0, 0x92, 0x35, 0x05, 0x11, 0xe3, 0x0a, 0xbe, 0xc1, 0xff, 0xf9, 0xe3, 0xa2, 0x6e, + 0x7f, 0xb2, 0x9f, 0x8c, 0x18, 0x30, 0x23, 0xc3, 0x58, 0x7e, 0x38, 0xda, 0x00, 0x77, 0xd9, 0xb4, + 0x76, 0x3e, 0x4e, 0x4b, 0x94, 0xb2, 0xbb, 0xc1, 0x94, 0xc6, 0x65, 0x1e, 0x77, 0xca, 0xf9, 0x92, + 0xee, 0xaa, 0xc0, 0x23, 0x2a, 0x28, 0x1b, 0xf6, 0xb3, 0xa7, 0x39, 0xc1, 0x22, 0x61, 0x16, 0x82, + 0x0a, 0xe8, 0xdb, 0x58, 0x47, 0xa6, 0x7c, 0xbe, 0xf9, 0xc9, 0x09, 0x1b, 0x46, 0x2d, 0x53, 0x8c, + 0xd7, 0x2b, 0x03, 0x74, 0x6a, 0xe7, 0x7f, 0x5e, 0x62, 0x29, 0x2c, 0x31, 0x15, 0x62, 0xa8, 0x46, + 0x50, 0x5d, 0xc8, 0x2d, 0xb8, 0x54, 0x33, 0x8a, 0xe4, 0x9f, 0x52, 0x35, 0xc9, 0x5b, 0x91, 0x17, + 0x8c, 0xcf, 0x2d, 0xd5, 0xca, 0xce, 0xf4, 0x03, 0xec, 0x9d, 0x18, 0x10, 0xc6, 0x27, 0x2b, 0x04, + 0x5b, 0x3b, 0x71, 0xf9, 0xdc, 0x6b, 0x80, 0xd6, 0x3f, 0xdd, 0x4a, 0x8e, 0x9a, 0xdb, 0x1e, 0x69, + 0x62, 0xa6, 0x95, 0x26, 0xd4, 0x31, 0x61, 0xc1, 0xa4, 0x1d, 0x57, 0x0d, 0x79, 0x38, 0xda, 0xd4, + 0xa4, 0x0e, 0x32, 0x9c, 0xd0, 0xe4, 0x0e, 0x65, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_ffdhe8192_p[1024] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xad, 0xf8, 0x54, 0x58, 0xa2, 0xbb, 0x4a, 0x9a, + 0xaf, 0xdc, 0x56, 0x20, 0x27, 0x3d, 0x3c, 0xf1, 0xd8, 0xb9, 0xc5, 0x83, 0xce, 0x2d, 0x36, 0x95, + 0xa9, 0xe1, 0x36, 0x41, 0x14, 0x64, 0x33, 0xfb, 0xcc, 0x93, 0x9d, 0xce, 0x24, 0x9b, 0x3e, 0xf9, + 0x7d, 0x2f, 0xe3, 0x63, 0x63, 0x0c, 0x75, 0xd8, 0xf6, 0x81, 0xb2, 0x02, 0xae, 0xc4, 0x61, 0x7a, + 0xd3, 0xdf, 0x1e, 0xd5, 0xd5, 0xfd, 0x65, 0x61, 0x24, 0x33, 0xf5, 0x1f, 0x5f, 0x06, 0x6e, 0xd0, + 0x85, 0x63, 0x65, 0x55, 0x3d, 0xed, 0x1a, 0xf3, 0xb5, 0x57, 0x13, 0x5e, 0x7f, 0x57, 0xc9, 0x35, + 0x98, 0x4f, 0x0c, 0x70, 0xe0, 0xe6, 0x8b, 0x77, 0xe2, 0xa6, 0x89, 0xda, 0xf3, 0xef, 0xe8, 0x72, + 0x1d, 0xf1, 0x58, 0xa1, 0x36, 0xad, 0xe7, 0x35, 0x30, 0xac, 0xca, 0x4f, 0x48, 0x3a, 0x79, 0x7a, + 0xbc, 0x0a, 0xb1, 0x82, 0xb3, 0x24, 0xfb, 0x61, 0xd1, 0x08, 0xa9, 0x4b, 0xb2, 0xc8, 0xe3, 0xfb, + 0xb9, 0x6a, 0xda, 0xb7, 0x60, 0xd7, 0xf4, 0x68, 0x1d, 0x4f, 0x42, 0xa3, 0xde, 0x39, 0x4d, 0xf4, + 0xae, 0x56, 0xed, 0xe7, 0x63, 0x72, 0xbb, 0x19, 0x0b, 0x07, 0xa7, 0xc8, 0xee, 0x0a, 0x6d, 0x70, + 0x9e, 0x02, 0xfc, 0xe1, 0xcd, 0xf7, 0xe2, 0xec, 0xc0, 0x34, 0x04, 0xcd, 0x28, 0x34, 0x2f, 0x61, + 0x91, 0x72, 0xfe, 0x9c, 0xe9, 0x85, 0x83, 0xff, 0x8e, 0x4f, 0x12, 0x32, 0xee, 0xf2, 0x81, 0x83, + 0xc3, 0xfe, 0x3b, 0x1b, 0x4c, 0x6f, 0xad, 0x73, 0x3b, 0xb5, 0xfc, 0xbc, 0x2e, 0xc2, 0x20, 0x05, + 0xc5, 0x8e, 0xf1, 0x83, 0x7d, 0x16, 0x83, 0xb2, 0xc6, 0xf3, 0x4a, 0x26, 0xc1, 0xb2, 0xef, 0xfa, + 0x88, 0x6b, 0x42, 0x38, 0x61, 0x1f, 0xcf, 0xdc, 0xde, 0x35, 0x5b, 0x3b, 0x65, 0x19, 0x03, 0x5b, + 0xbc, 0x34, 0xf4, 0xde, 0xf9, 0x9c, 0x02, 0x38, 0x61, 0xb4, 0x6f, 0xc9, 0xd6, 0xe6, 0xc9, 0x07, + 0x7a, 0xd9, 0x1d, 0x26, 0x91, 0xf7, 0xf7, 0xee, 0x59, 0x8c, 0xb0, 0xfa, 0xc1, 0x86, 0xd9, 0x1c, + 0xae, 0xfe, 0x13, 0x09, 0x85, 0x13, 0x92, 0x70, 0xb4, 0x13, 0x0c, 0x93, 0xbc, 0x43, 0x79, 0x44, + 0xf4, 0xfd, 0x44, 0x52, 0xe2, 0xd7, 0x4d, 0xd3, 0x64, 0xf2, 0xe2, 0x1e, 0x71, 0xf5, 0x4b, 0xff, + 0x5c, 0xae, 0x82, 0xab, 0x9c, 0x9d, 0xf6, 0x9e, 0xe8, 0x6d, 0x2b, 0xc5, 0x22, 0x36, 0x3a, 0x0d, + 0xab, 0xc5, 0x21, 0x97, 0x9b, 0x0d, 0xea, 0xda, 0x1d, 0xbf, 0x9a, 0x42, 0xd5, 0xc4, 0x48, 0x4e, + 0x0a, 0xbc, 0xd0, 0x6b, 0xfa, 0x53, 0xdd, 0xef, 0x3c, 0x1b, 0x20, 0xee, 0x3f, 0xd5, 0x9d, 0x7c, + 0x25, 0xe4, 0x1d, 0x2b, 0x66, 0x9e, 0x1e, 0xf1, 0x6e, 0x6f, 0x52, 0xc3, 0x16, 0x4d, 0xf4, 0xfb, + 0x79, 0x30, 0xe9, 0xe4, 0xe5, 0x88, 0x57, 0xb6, 0xac, 0x7d, 0x5f, 0x42, 0xd6, 0x9f, 0x6d, 0x18, + 0x77, 0x63, 0xcf, 0x1d, 0x55, 0x03, 0x40, 0x04, 0x87, 0xf5, 0x5b, 0xa5, 0x7e, 0x31, 0xcc, 0x7a, + 0x71, 0x35, 0xc8, 0x86, 0xef, 0xb4, 0x31, 0x8a, 0xed, 0x6a, 0x1e, 0x01, 0x2d, 0x9e, 0x68, 0x32, + 0xa9, 0x07, 0x60, 0x0a, 0x91, 0x81, 0x30, 0xc4, 0x6d, 0xc7, 0x78, 0xf9, 0x71, 0xad, 0x00, 0x38, + 0x09, 0x29, 0x99, 0xa3, 0x33, 0xcb, 0x8b, 0x7a, 0x1a, 0x1d, 0xb9, 0x3d, 0x71, 0x40, 0x00, 0x3c, + 0x2a, 0x4e, 0xce, 0xa9, 0xf9, 0x8d, 0x0a, 0xcc, 0x0a, 0x82, 0x91, 0xcd, 0xce, 0xc9, 0x7d, 0xcf, + 0x8e, 0xc9, 0xb5, 0x5a, 0x7f, 0x88, 0xa4, 0x6b, 0x4d, 0xb5, 0xa8, 0x51, 0xf4, 0x41, 0x82, 0xe1, + 0xc6, 0x8a, 0x00, 0x7e, 0x5e, 0x0d, 0xd9, 0x02, 0x0b, 0xfd, 0x64, 0xb6, 0x45, 0x03, 0x6c, 0x7a, + 0x4e, 0x67, 0x7d, 0x2c, 0x38, 0x53, 0x2a, 0x3a, 0x23, 0xba, 0x44, 0x42, 0xca, 0xf5, 0x3e, 0xa6, + 0x3b, 0xb4, 0x54, 0x32, 0x9b, 0x76, 0x24, 0xc8, 0x91, 0x7b, 0xdd, 0x64, 0xb1, 0xc0, 0xfd, 0x4c, + 0xb3, 0x8e, 0x8c, 0x33, 0x4c, 0x70, 0x1c, 0x3a, 0xcd, 0xad, 0x06, 0x57, 0xfc, 0xcf, 0xec, 0x71, + 0x9b, 0x1f, 0x5c, 0x3e, 0x4e, 0x46, 0x04, 0x1f, 0x38, 0x81, 0x47, 0xfb, 0x4c, 0xfd, 0xb4, 0x77, + 0xa5, 0x24, 0x71, 0xf7, 0xa9, 0xa9, 0x69, 0x10, 0xb8, 0x55, 0x32, 0x2e, 0xdb, 0x63, 0x40, 0xd8, + 0xa0, 0x0e, 0xf0, 0x92, 0x35, 0x05, 0x11, 0xe3, 0x0a, 0xbe, 0xc1, 0xff, 0xf9, 0xe3, 0xa2, 0x6e, + 0x7f, 0xb2, 0x9f, 0x8c, 0x18, 0x30, 0x23, 0xc3, 0x58, 0x7e, 0x38, 0xda, 0x00, 0x77, 0xd9, 0xb4, + 0x76, 0x3e, 0x4e, 0x4b, 0x94, 0xb2, 0xbb, 0xc1, 0x94, 0xc6, 0x65, 0x1e, 0x77, 0xca, 0xf9, 0x92, + 0xee, 0xaa, 0xc0, 0x23, 0x2a, 0x28, 0x1b, 0xf6, 0xb3, 0xa7, 0x39, 0xc1, 0x22, 0x61, 0x16, 0x82, + 0x0a, 0xe8, 0xdb, 0x58, 0x47, 0xa6, 0x7c, 0xbe, 0xf9, 0xc9, 0x09, 0x1b, 0x46, 0x2d, 0x53, 0x8c, + 0xd7, 0x2b, 0x03, 0x74, 0x6a, 0xe7, 0x7f, 0x5e, 0x62, 0x29, 0x2c, 0x31, 0x15, 0x62, 0xa8, 0x46, + 0x50, 0x5d, 0xc8, 0x2d, 0xb8, 0x54, 0x33, 0x8a, 0xe4, 0x9f, 0x52, 0x35, 0xc9, 0x5b, 0x91, 0x17, + 0x8c, 0xcf, 0x2d, 0xd5, 0xca, 0xce, 0xf4, 0x03, 0xec, 0x9d, 0x18, 0x10, 0xc6, 0x27, 0x2b, 0x04, + 0x5b, 0x3b, 0x71, 0xf9, 0xdc, 0x6b, 0x80, 0xd6, 0x3f, 0xdd, 0x4a, 0x8e, 0x9a, 0xdb, 0x1e, 0x69, + 0x62, 0xa6, 0x95, 0x26, 0xd4, 0x31, 0x61, 0xc1, 0xa4, 0x1d, 0x57, 0x0d, 0x79, 0x38, 0xda, 0xd4, + 0xa4, 0x0e, 0x32, 0x9c, 0xcf, 0xf4, 0x6a, 0xaa, 0x36, 0xad, 0x00, 0x4c, 0xf6, 0x00, 0xc8, 0x38, + 0x1e, 0x42, 0x5a, 0x31, 0xd9, 0x51, 0xae, 0x64, 0xfd, 0xb2, 0x3f, 0xce, 0xc9, 0x50, 0x9d, 0x43, + 0x68, 0x7f, 0xeb, 0x69, 0xed, 0xd1, 0xcc, 0x5e, 0x0b, 0x8c, 0xc3, 0xbd, 0xf6, 0x4b, 0x10, 0xef, + 0x86, 0xb6, 0x31, 0x42, 0xa3, 0xab, 0x88, 0x29, 0x55, 0x5b, 0x2f, 0x74, 0x7c, 0x93, 0x26, 0x65, + 0xcb, 0x2c, 0x0f, 0x1c, 0xc0, 0x1b, 0xd7, 0x02, 0x29, 0x38, 0x88, 0x39, 0xd2, 0xaf, 0x05, 0xe4, + 0x54, 0x50, 0x4a, 0xc7, 0x8b, 0x75, 0x82, 0x82, 0x28, 0x46, 0xc0, 0xba, 0x35, 0xc3, 0x5f, 0x5c, + 0x59, 0x16, 0x0c, 0xc0, 0x46, 0xfd, 0x82, 0x51, 0x54, 0x1f, 0xc6, 0x8c, 0x9c, 0x86, 0xb0, 0x22, + 0xbb, 0x70, 0x99, 0x87, 0x6a, 0x46, 0x0e, 0x74, 0x51, 0xa8, 0xa9, 0x31, 0x09, 0x70, 0x3f, 0xee, + 0x1c, 0x21, 0x7e, 0x6c, 0x38, 0x26, 0xe5, 0x2c, 0x51, 0xaa, 0x69, 0x1e, 0x0e, 0x42, 0x3c, 0xfc, + 0x99, 0xe9, 0xe3, 0x16, 0x50, 0xc1, 0x21, 0x7b, 0x62, 0x48, 0x16, 0xcd, 0xad, 0x9a, 0x95, 0xf9, + 0xd5, 0xb8, 0x01, 0x94, 0x88, 0xd9, 0xc0, 0xa0, 0xa1, 0xfe, 0x30, 0x75, 0xa5, 0x77, 0xe2, 0x31, + 0x83, 0xf8, 0x1d, 0x4a, 0x3f, 0x2f, 0xa4, 0x57, 0x1e, 0xfc, 0x8c, 0xe0, 0xba, 0x8a, 0x4f, 0xe8, + 0xb6, 0x85, 0x5d, 0xfe, 0x72, 0xb0, 0xa6, 0x6e, 0xde, 0xd2, 0xfb, 0xab, 0xfb, 0xe5, 0x8a, 0x30, + 0xfa, 0xfa, 0xbe, 0x1c, 0x5d, 0x71, 0xa8, 0x7e, 0x2f, 0x74, 0x1e, 0xf8, 0xc1, 0xfe, 0x86, 0xfe, + 0xa6, 0xbb, 0xfd, 0xe5, 0x30, 0x67, 0x7f, 0x0d, 0x97, 0xd1, 0x1d, 0x49, 0xf7, 0xa8, 0x44, 0x3d, + 0x08, 0x22, 0xe5, 0x06, 0xa9, 0xf4, 0x61, 0x4e, 0x01, 0x1e, 0x2a, 0x94, 0x83, 0x8f, 0xf8, 0x8c, + 0xd6, 0x8c, 0x8b, 0xb7, 0xc5, 0xc6, 0x42, 0x4c, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; + +static const unsigned char scr_dh_dh_1024_160_p[128] = { + 0xb1, 0x0b, 0x8f, 0x96, 0xa0, 0x80, 0xe0, 0x1d, 0xde, 0x92, 0xde, 0x5e, 0xae, 0x5d, 0x54, 0xec, + 0x52, 0xc9, 0x9f, 0xbc, 0xfb, 0x06, 0xa3, 0xc6, 0x9a, 0x6a, 0x9d, 0xca, 0x52, 0xd2, 0x3b, 0x61, + 0x60, 0x73, 0xe2, 0x86, 0x75, 0xa2, 0x3d, 0x18, 0x98, 0x38, 0xef, 0x1e, 0x2e, 0xe6, 0x52, 0xc0, + 0x13, 0xec, 0xb4, 0xae, 0xa9, 0x06, 0x11, 0x23, 0x24, 0x97, 0x5c, 0x3c, 0xd4, 0x9b, 0x83, 0xbf, + 0xac, 0xcb, 0xdd, 0x7d, 0x90, 0xc4, 0xbd, 0x70, 0x98, 0x48, 0x8e, 0x9c, 0x21, 0x9a, 0x73, 0x72, + 0x4e, 0xff, 0xd6, 0xfa, 0xe5, 0x64, 0x47, 0x38, 0xfa, 0xa3, 0x1a, 0x4f, 0xf5, 0x5b, 0xcc, 0xc0, + 0xa1, 0x51, 0xaf, 0x5f, 0x0d, 0xc8, 0xb4, 0xbd, 0x45, 0xbf, 0x37, 0xdf, 0x36, 0x5c, 0x1a, 0x65, + 0xe6, 0x8c, 0xfd, 0xa7, 0x6d, 0x4d, 0xa7, 0x08, 0xdf, 0x1f, 0xb2, 0xbc, 0x2e, 0x4a, 0x43, 0x71, +}; + +static const unsigned char scr_dh_dh_1024_160_g[128] = { + 0xa4, 0xd1, 0xcb, 0xd5, 0xc3, 0xfd, 0x34, 0x12, 0x67, 0x65, 0xa4, 0x42, 0xef, 0xb9, 0x99, 0x05, + 0xf8, 0x10, 0x4d, 0xd2, 0x58, 0xac, 0x50, 0x7f, 0xd6, 0x40, 0x6c, 0xff, 0x14, 0x26, 0x6d, 0x31, + 0x26, 0x6f, 0xea, 0x1e, 0x5c, 0x41, 0x56, 0x4b, 0x77, 0x7e, 0x69, 0x0f, 0x55, 0x04, 0xf2, 0x13, + 0x16, 0x02, 0x17, 0xb4, 0xb0, 0x1b, 0x88, 0x6a, 0x5e, 0x91, 0x54, 0x7f, 0x9e, 0x27, 0x49, 0xf4, + 0xd7, 0xfb, 0xd7, 0xd3, 0xb9, 0xa9, 0x2e, 0xe1, 0x90, 0x9d, 0x0d, 0x22, 0x63, 0xf8, 0x0a, 0x76, + 0xa6, 0xa2, 0x4c, 0x08, 0x7a, 0x09, 0x1f, 0x53, 0x1d, 0xbf, 0x0a, 0x01, 0x69, 0xb6, 0xa2, 0x8a, + 0xd6, 0x62, 0xa4, 0xd1, 0x8e, 0x73, 0xaf, 0xa3, 0x2d, 0x77, 0x9d, 0x59, 0x18, 0xd0, 0x8b, 0xc8, + 0x85, 0x8f, 0x4d, 0xce, 0xf9, 0x7c, 0x2a, 0x24, 0x85, 0x5e, 0x6e, 0xeb, 0x22, 0xb3, 0xb2, 0xe5, +}; + +static const unsigned char scr_dh_dh_1024_160_q[20] = { + 0xf5, 0x18, 0xaa, 0x87, 0x81, 0xa8, 0xdf, 0x27, 0x8a, 0xba, 0x4e, 0x7d, 0x64, 0xb7, 0xcb, 0x9d, + 0x49, 0x46, 0x23, 0x53, +}; + +static const unsigned char scr_dh_dh_2048_224_p[256] = { + 0xad, 0x10, 0x7e, 0x1e, 0x91, 0x23, 0xa9, 0xd0, 0xd6, 0x60, 0xfa, 0xa7, 0x95, 0x59, 0xc5, 0x1f, + 0xa2, 0x0d, 0x64, 0xe5, 0x68, 0x3b, 0x9f, 0xd1, 0xb5, 0x4b, 0x15, 0x97, 0xb6, 0x1d, 0x0a, 0x75, + 0xe6, 0xfa, 0x14, 0x1d, 0xf9, 0x5a, 0x56, 0xdb, 0xaf, 0x9a, 0x3c, 0x40, 0x7b, 0xa1, 0xdf, 0x15, + 0xeb, 0x3d, 0x68, 0x8a, 0x30, 0x9c, 0x18, 0x0e, 0x1d, 0xe6, 0xb8, 0x5a, 0x12, 0x74, 0xa0, 0xa6, + 0x6d, 0x3f, 0x81, 0x52, 0xad, 0x6a, 0xc2, 0x12, 0x90, 0x37, 0xc9, 0xed, 0xef, 0xda, 0x4d, 0xf8, + 0xd9, 0x1e, 0x8f, 0xef, 0x55, 0xb7, 0x39, 0x4b, 0x7a, 0xd5, 0xb7, 0xd0, 0xb6, 0xc1, 0x22, 0x07, + 0xc9, 0xf9, 0x8d, 0x11, 0xed, 0x34, 0xdb, 0xf6, 0xc6, 0xba, 0x0b, 0x2c, 0x8b, 0xbc, 0x27, 0xbe, + 0x6a, 0x00, 0xe0, 0xa0, 0xb9, 0xc4, 0x97, 0x08, 0xb3, 0xbf, 0x8a, 0x31, 0x70, 0x91, 0x88, 0x36, + 0x81, 0x28, 0x61, 0x30, 0xbc, 0x89, 0x85, 0xdb, 0x16, 0x02, 0xe7, 0x14, 0x41, 0x5d, 0x93, 0x30, + 0x27, 0x82, 0x73, 0xc7, 0xde, 0x31, 0xef, 0xdc, 0x73, 0x10, 0xf7, 0x12, 0x1f, 0xd5, 0xa0, 0x74, + 0x15, 0x98, 0x7d, 0x9a, 0xdc, 0x0a, 0x48, 0x6d, 0xcd, 0xf9, 0x3a, 0xcc, 0x44, 0x32, 0x83, 0x87, + 0x31, 0x5d, 0x75, 0xe1, 0x98, 0xc6, 0x41, 0xa4, 0x80, 0xcd, 0x86, 0xa1, 0xb9, 0xe5, 0x87, 0xe8, + 0xbe, 0x60, 0xe6, 0x9c, 0xc9, 0x28, 0xb2, 0xb9, 0xc5, 0x21, 0x72, 0xe4, 0x13, 0x04, 0x2e, 0x9b, + 0x23, 0xf1, 0x0b, 0x0e, 0x16, 0xe7, 0x97, 0x63, 0xc9, 0xb5, 0x3d, 0xcf, 0x4b, 0xa8, 0x0a, 0x29, + 0xe3, 0xfb, 0x73, 0xc1, 0x6b, 0x8e, 0x75, 0xb9, 0x7e, 0xf3, 0x63, 0xe2, 0xff, 0xa3, 0x1f, 0x71, + 0xcf, 0x9d, 0xe5, 0x38, 0x4e, 0x71, 0xb8, 0x1c, 0x0a, 0xc4, 0xdf, 0xfe, 0x0c, 0x10, 0xe6, 0x4f, +}; + +static const unsigned char scr_dh_dh_2048_224_g[256] = { + 0xac, 0x40, 0x32, 0xef, 0x4f, 0x2d, 0x9a, 0xe3, 0x9d, 0xf3, 0x0b, 0x5c, 0x8f, 0xfd, 0xac, 0x50, + 0x6c, 0xde, 0xbe, 0x7b, 0x89, 0x99, 0x8c, 0xaf, 0x74, 0x86, 0x6a, 0x08, 0xcf, 0xe4, 0xff, 0xe3, + 0xa6, 0x82, 0x4a, 0x4e, 0x10, 0xb9, 0xa6, 0xf0, 0xdd, 0x92, 0x1f, 0x01, 0xa7, 0x0c, 0x4a, 0xfa, + 0xab, 0x73, 0x9d, 0x77, 0x00, 0xc2, 0x9f, 0x52, 0xc5, 0x7d, 0xb1, 0x7c, 0x62, 0x0a, 0x86, 0x52, + 0xbe, 0x5e, 0x90, 0x01, 0xa8, 0xd6, 0x6a, 0xd7, 0xc1, 0x76, 0x69, 0x10, 0x19, 0x99, 0x02, 0x4a, + 0xf4, 0xd0, 0x27, 0x27, 0x5a, 0xc1, 0x34, 0x8b, 0xb8, 0xa7, 0x62, 0xd0, 0x52, 0x1b, 0xc9, 0x8a, + 0xe2, 0x47, 0x15, 0x04, 0x22, 0xea, 0x1e, 0xd4, 0x09, 0x93, 0x9d, 0x54, 0xda, 0x74, 0x60, 0xcd, + 0xb5, 0xf6, 0xc6, 0xb2, 0x50, 0x71, 0x7c, 0xbe, 0xf1, 0x80, 0xeb, 0x34, 0x11, 0x8e, 0x98, 0xd1, + 0x19, 0x52, 0x9a, 0x45, 0xd6, 0xf8, 0x34, 0x56, 0x6e, 0x30, 0x25, 0xe3, 0x16, 0xa3, 0x30, 0xef, + 0xbb, 0x77, 0xa8, 0x6f, 0x0c, 0x1a, 0xb1, 0x5b, 0x05, 0x1a, 0xe3, 0xd4, 0x28, 0xc8, 0xf8, 0xac, + 0xb7, 0x0a, 0x81, 0x37, 0x15, 0x0b, 0x8e, 0xeb, 0x10, 0xe1, 0x83, 0xed, 0xd1, 0x99, 0x63, 0xdd, + 0xd9, 0xe2, 0x63, 0xe4, 0x77, 0x05, 0x89, 0xef, 0x6a, 0xa2, 0x1e, 0x7f, 0x5f, 0x2f, 0xf3, 0x81, + 0xb5, 0x39, 0xcc, 0xe3, 0x40, 0x9d, 0x13, 0xcd, 0x56, 0x6a, 0xfb, 0xb4, 0x8d, 0x6c, 0x01, 0x91, + 0x81, 0xe1, 0xbc, 0xfe, 0x94, 0xb3, 0x02, 0x69, 0xed, 0xfe, 0x72, 0xfe, 0x9b, 0x6a, 0xa4, 0xbd, + 0x7b, 0x5a, 0x0f, 0x1c, 0x71, 0xcf, 0xff, 0x4c, 0x19, 0xc4, 0x18, 0xe1, 0xf6, 0xec, 0x01, 0x79, + 0x81, 0xbc, 0x08, 0x7f, 0x2a, 0x70, 0x65, 0xb3, 0x84, 0xb8, 0x90, 0xd3, 0x19, 0x1f, 0x2b, 0xfa, +}; + +static const unsigned char scr_dh_dh_2048_224_q[28] = { + 0x80, 0x1c, 0x0d, 0x34, 0xc5, 0x8d, 0x93, 0xfe, 0x99, 0x71, 0x77, 0x10, 0x1f, 0x80, 0x53, 0x5a, + 0x47, 0x38, 0xce, 0xbc, 0xbf, 0x38, 0x9a, 0x99, 0xb3, 0x63, 0x71, 0xeb, +}; + +static const unsigned char scr_dh_dh_2048_256_p[256] = { + 0x87, 0xa8, 0xe6, 0x1d, 0xb4, 0xb6, 0x66, 0x3c, 0xff, 0xbb, 0xd1, 0x9c, 0x65, 0x19, 0x59, 0x99, + 0x8c, 0xee, 0xf6, 0x08, 0x66, 0x0d, 0xd0, 0xf2, 0x5d, 0x2c, 0xee, 0xd4, 0x43, 0x5e, 0x3b, 0x00, + 0xe0, 0x0d, 0xf8, 0xf1, 0xd6, 0x19, 0x57, 0xd4, 0xfa, 0xf7, 0xdf, 0x45, 0x61, 0xb2, 0xaa, 0x30, + 0x16, 0xc3, 0xd9, 0x11, 0x34, 0x09, 0x6f, 0xaa, 0x3b, 0xf4, 0x29, 0x6d, 0x83, 0x0e, 0x9a, 0x7c, + 0x20, 0x9e, 0x0c, 0x64, 0x97, 0x51, 0x7a, 0xbd, 0x5a, 0x8a, 0x9d, 0x30, 0x6b, 0xcf, 0x67, 0xed, + 0x91, 0xf9, 0xe6, 0x72, 0x5b, 0x47, 0x58, 0xc0, 0x22, 0xe0, 0xb1, 0xef, 0x42, 0x75, 0xbf, 0x7b, + 0x6c, 0x5b, 0xfc, 0x11, 0xd4, 0x5f, 0x90, 0x88, 0xb9, 0x41, 0xf5, 0x4e, 0xb1, 0xe5, 0x9b, 0xb8, + 0xbc, 0x39, 0xa0, 0xbf, 0x12, 0x30, 0x7f, 0x5c, 0x4f, 0xdb, 0x70, 0xc5, 0x81, 0xb2, 0x3f, 0x76, + 0xb6, 0x3a, 0xca, 0xe1, 0xca, 0xa6, 0xb7, 0x90, 0x2d, 0x52, 0x52, 0x67, 0x35, 0x48, 0x8a, 0x0e, + 0xf1, 0x3c, 0x6d, 0x9a, 0x51, 0xbf, 0xa4, 0xab, 0x3a, 0xd8, 0x34, 0x77, 0x96, 0x52, 0x4d, 0x8e, + 0xf6, 0xa1, 0x67, 0xb5, 0xa4, 0x18, 0x25, 0xd9, 0x67, 0xe1, 0x44, 0xe5, 0x14, 0x05, 0x64, 0x25, + 0x1c, 0xca, 0xcb, 0x83, 0xe6, 0xb4, 0x86, 0xf6, 0xb3, 0xca, 0x3f, 0x79, 0x71, 0x50, 0x60, 0x26, + 0xc0, 0xb8, 0x57, 0xf6, 0x89, 0x96, 0x28, 0x56, 0xde, 0xd4, 0x01, 0x0a, 0xbd, 0x0b, 0xe6, 0x21, + 0xc3, 0xa3, 0x96, 0x0a, 0x54, 0xe7, 0x10, 0xc3, 0x75, 0xf2, 0x63, 0x75, 0xd7, 0x01, 0x41, 0x03, + 0xa4, 0xb5, 0x43, 0x30, 0xc1, 0x98, 0xaf, 0x12, 0x61, 0x16, 0xd2, 0x27, 0x6e, 0x11, 0x71, 0x5f, + 0x69, 0x38, 0x77, 0xfa, 0xd7, 0xef, 0x09, 0xca, 0xdb, 0x09, 0x4a, 0xe9, 0x1e, 0x1a, 0x15, 0x97, +}; + +static const unsigned char scr_dh_dh_2048_256_g[256] = { + 0x3f, 0xb3, 0x2c, 0x9b, 0x73, 0x13, 0x4d, 0x0b, 0x2e, 0x77, 0x50, 0x66, 0x60, 0xed, 0xbd, 0x48, + 0x4c, 0xa7, 0xb1, 0x8f, 0x21, 0xef, 0x20, 0x54, 0x07, 0xf4, 0x79, 0x3a, 0x1a, 0x0b, 0xa1, 0x25, + 0x10, 0xdb, 0xc1, 0x50, 0x77, 0xbe, 0x46, 0x3f, 0xff, 0x4f, 0xed, 0x4a, 0xac, 0x0b, 0xb5, 0x55, + 0xbe, 0x3a, 0x6c, 0x1b, 0x0c, 0x6b, 0x47, 0xb1, 0xbc, 0x37, 0x73, 0xbf, 0x7e, 0x8c, 0x6f, 0x62, + 0x90, 0x12, 0x28, 0xf8, 0xc2, 0x8c, 0xbb, 0x18, 0xa5, 0x5a, 0xe3, 0x13, 0x41, 0x00, 0x0a, 0x65, + 0x01, 0x96, 0xf9, 0x31, 0xc7, 0x7a, 0x57, 0xf2, 0xdd, 0xf4, 0x63, 0xe5, 0xe9, 0xec, 0x14, 0x4b, + 0x77, 0x7d, 0xe6, 0x2a, 0xaa, 0xb8, 0xa8, 0x62, 0x8a, 0xc3, 0x76, 0xd2, 0x82, 0xd6, 0xed, 0x38, + 0x64, 0xe6, 0x79, 0x82, 0x42, 0x8e, 0xbc, 0x83, 0x1d, 0x14, 0x34, 0x8f, 0x6f, 0x2f, 0x91, 0x93, + 0xb5, 0x04, 0x5a, 0xf2, 0x76, 0x71, 0x64, 0xe1, 0xdf, 0xc9, 0x67, 0xc1, 0xfb, 0x3f, 0x2e, 0x55, + 0xa4, 0xbd, 0x1b, 0xff, 0xe8, 0x3b, 0x9c, 0x80, 0xd0, 0x52, 0xb9, 0x85, 0xd1, 0x82, 0xea, 0x0a, + 0xdb, 0x2a, 0x3b, 0x73, 0x13, 0xd3, 0xfe, 0x14, 0xc8, 0x48, 0x4b, 0x1e, 0x05, 0x25, 0x88, 0xb9, + 0xb7, 0xd2, 0xbb, 0xd2, 0xdf, 0x01, 0x61, 0x99, 0xec, 0xd0, 0x6e, 0x15, 0x57, 0xcd, 0x09, 0x15, + 0xb3, 0x35, 0x3b, 0xbb, 0x64, 0xe0, 0xec, 0x37, 0x7f, 0xd0, 0x28, 0x37, 0x0d, 0xf9, 0x2b, 0x52, + 0xc7, 0x89, 0x14, 0x28, 0xcd, 0xc6, 0x7e, 0xb6, 0x18, 0x4b, 0x52, 0x3d, 0x1d, 0xb2, 0x46, 0xc3, + 0x2f, 0x63, 0x07, 0x84, 0x90, 0xf0, 0x0e, 0xf8, 0xd6, 0x47, 0xd1, 0x48, 0xd4, 0x79, 0x54, 0x51, + 0x5e, 0x23, 0x27, 0xcf, 0xef, 0x98, 0xc5, 0x82, 0x66, 0x4b, 0x4c, 0x0f, 0x6c, 0xc4, 0x16, 0x59, +}; + +static const unsigned char scr_dh_dh_2048_256_q[32] = { + 0x8c, 0xf8, 0x36, 0x42, 0xa7, 0x09, 0xa0, 0x97, 0xb4, 0x47, 0x99, 0x76, 0x40, 0x12, 0x9d, 0xa2, + 0x99, 0xb1, 0xa4, 0x7d, 0x1e, 0xb3, 0x75, 0x0b, 0xa3, 0x08, 0xb0, 0xfe, 0x64, 0xf5, 0xfb, 0xd3, +}; + +#endif diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 37e9eacceb..4b618a809b 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -155,6 +155,8 @@ typedef enum { X(DATE, 30) /* JavaScript Date with stable native identity */ \ X(DECIPHER, 46) /* node:crypto Decipheriv (scr_cipher.c) */ \ X(DGRAM_SOCKET, 62) /* dgram.Socket (scr_dgram.c) */ \ + X(DIFFIE_HELLMAN, 65) /* node:crypto DiffieHellman (scr_dh.c) */ \ + X(DIFFIE_HELLMAN_GROUP, 66) /* node:crypto DiffieHellmanGroup (scr_dh.c) */ \ X(DIR, 44) /* fs.Dir (scr_lib.c) */ \ X(DISPOSABLE_STACK, 49) /* DisposableStack (scr_symbol.c) */ \ X(ECDH, 64) /* node:crypto ECDH (scr_ecdh.c) */ \ diff --git a/packages/runtime/src/scr_ecdh.c b/packages/runtime/src/scr_ecdh.c index fc87975714..5edbadba65 100644 --- a/packages/runtime/src/scr_ecdh.c +++ b/packages/runtime/src/scr_ecdh.c @@ -21,6 +21,7 @@ #include "mbedtls/ecp.h" #include "mbedtls/platform_util.h" +#include "scr_crypto_args.h" #include "scr_ecdh_names.h" typedef struct { @@ -73,6 +74,9 @@ static int se_random(void *state, unsigned char *out, size_t len) { return 0; } +/* The value the SC2020 error for an unlowered encoding or format names. */ +#define SE_SUBJECT "An ECDH encoding or format" + /* ── errors ───────────────────────────────────────────────────────────── */ static void se_throw(int kind, const char *code, const char *message) { @@ -83,51 +87,6 @@ static void se_operation_failed(const char *message) { se_throw(SCR_ERR_ERROR, "ERR_CRYPTO_OPERATION_FAILED", message); } -/* An object's toString the program defined, as an own or inherited - * function property. */ -static bool se_custom_to_string(ScrDyn *value) { - if (value->kind != SCR_DYN_OBJ && value->kind != SCR_DYN_TYPED_REF) return false; - ScrDyn *member = scr_dyn_obj_read(value, "toString", 8); - bool custom = member && member->kind == SCR_DYN_FUNC; - scr_dyn_release(member); - return custom; -} - -/* Whether value is a class instance whose class defines toString. */ -static bool se_class_to_string(const ScrDyn *value) { - ScrStr *name = scr_str_new("toString", 8); - bool method = scr_dyn_typed_ref_class_has(value, name); - scr_str_release(name); - return method; -} - -/* `prefix` and the value as util.format's %s renders it: strings and - * objects with a program-defined toString as String(value), numbers as - * util.inspect does, other values inspected at depth 0. Functions and - * class instances that define toString have no lowering. */ -static void se_throw_formatted(const char *code, const char *prefix, const ScrDyn *value) { - ScrDyn *v = (ScrDyn *)value; - if (v->kind == SCR_DYN_FUNC || se_class_to_string(v)) { - static const char message[] = "An ECDH encoding or format that is a function or an instance of a class " - "defining toString has no lowering; pass the name as a string"; - scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); - return; - } - ScrStr *text; - if (v->kind == SCR_DYN_STR || se_custom_to_string(v)) text = scr_dyn_string_coerce_js(v); - else if (v->kind == SCR_DYN_NUM) text = scr_insp_f64(v->v.num); - else text = scr_insp_dyn_s(v, 0); - if (!text) return; - ScrJsonBuf b; - scr_jb_init(&b); - scr_jb_puts(&b, prefix); - scr_jb_put_str(&b, text); - scr_str_release(text); - ScrStr *message = scr_jb_finish(&b); - scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, code); - scr_str_release(message); -} - /* ── names and encodings ──────────────────────────────────────────────── */ static int se_name_cmp(const void *key, const void *entry) { @@ -198,129 +157,16 @@ static bool se_load_group(const ScrStr *name, mbedtls_ecp_group *grp, const char return false; } -static bool se_ascii_eq_ci(const char *a, size_t a_len, const char *b) { - size_t b_len = strlen(b); - if (a_len != b_len) return false; - for (size_t i = 0; i < a_len; i++) { - char x = a[i]; - if (x >= 'A' && x <= 'Z') x = (char)(x - 'A' + 'a'); - if (x != b[i]) return false; - } - return true; -} - -static bool se_str_is(const ScrStr *text, const char *literal) { - size_t len = strlen(literal); - return text->len == len && memcmp(text->data, literal, len) == 0; -} - -/* Node's normalizeEncoding: the canonical name, or NULL. */ -static const char *se_normalize_encoding(const ScrStr *name) { - static const struct { const char *from; const char *to; } map[] = { - {"utf8", "utf8"}, {"utf-8", "utf8"}, {"hex", "hex"}, - {"base64", "base64"}, {"base64url", "base64url"}, {"latin1", "latin1"}, - {"binary", "latin1"}, {"ascii", "ascii"}, {"utf16le", "utf16le"}, - {"utf-16le", "utf16le"}, {"ucs2", "utf16le"}, {"ucs-2", "utf16le"}, - }; - for (size_t i = 0; i < sizeof map / sizeof map[0]; i++) - if (se_ascii_eq_ci(name->data, name->len, map[i].from)) return map[i].to; - return NULL; -} - -/* The value a typed-ref box holds when it is a string, view, ArrayBuffer - * or native handle (+1). Other values are retained as passed, so argument - * errors describe the original value. NULL after throwing. */ -static ScrDyn *se_unbox(ScrDyn *value) { - if (value->kind != SCR_DYN_TYPED_REF) return scr_dyn_retain(value); - ScrDyn *view = scr_dyn_typed_ref_materialize(value); - if (!view) return NULL; - if (view->kind == SCR_DYN_STR || view->kind == SCR_DYN_BYTES || view->kind == SCR_DYN_HANDLE || - scr_buffer_storage_is(view)) - return view; - scr_dyn_release(view); - return scr_dyn_retain(value); -} - -/* getArrayBufferOrView(key, 'key', encoding): a string is Buffer.from(key, - * encoding) with 'buffer' read as utf8; views and ArrayBuffers are their - * bytes. +1 or NULL after throwing. */ -static ScrBytes *se_key_bytes(const ScrDyn *key, const ScrDyn *encoding) { - if (key->kind == SCR_DYN_STR) { - /* Buffer.from(string, encoding): an encoding that is not a non-empty - * string reads as utf8. */ - const char *name = "utf8"; - if (encoding->kind == SCR_DYN_STR && encoding->v.str->len > 0 && !se_str_is(encoding->v.str, "buffer")) { - name = se_normalize_encoding(encoding->v.str); - if (!name) { - se_throw_formatted("ERR_UNKNOWN_ENCODING", "Unknown encoding: ", encoding); - return NULL; - } - } - ScrStr *canonical = scr_str_new(name, strlen(name)); - ScrBytes *bytes = scr_bytes_from_str(key->v.str, canonical); - scr_str_release(canonical); - return bytes; - } - if (key->kind == SCR_DYN_BYTES || scr_buffer_storage_is(key)) return scr_bytes_buffer_source(key); - scr_dyn_arg_type_fail("key", "of type string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView", key); - return NULL; -} - -/* encode(buffer, encoding): the Buffer, or buffer.toString(encoding) for - * a truthy encoding other than 'buffer'. Consumes bytes. */ -static ScrDyn *se_encode(ScrBytes *bytes, const ScrDyn *encoding) { - if (!scr_dyn_truthy(encoding) || (encoding->kind == SCR_DYN_STR && se_str_is(encoding->v.str, "buffer"))) { - ScrDyn *result = scr_dyn_new_buffer(bytes); - scr_bytes_release(bytes); - return result; - } - /* toString answers "" for no bytes before it reads the encoding. */ - ScrStr *text = NULL; - if (bytes->len == 0) { - text = scr_str_new("", 0); - } else { - /* getEncodingOps reads `encoding + ''`. */ - ScrStr *name = encoding->kind == SCR_DYN_STR ? scr_str_retain(encoding->v.str) : NULL; - if (!name) { - if (encoding->kind == SCR_DYN_FUNC || se_class_to_string(encoding)) { - se_throw_formatted("ERR_UNKNOWN_ENCODING", "Unknown encoding: ", encoding); - scr_bytes_release(bytes); - return NULL; - } - name = scr_dyn_string_coerce_js(encoding); - if (!name) { - scr_bytes_release(bytes); - return NULL; - } - } - const char *canonical = se_normalize_encoding(name); - scr_str_release(name); - if (!canonical) { - se_throw_formatted("ERR_UNKNOWN_ENCODING", "Unknown encoding: ", encoding); - scr_bytes_release(bytes); - return NULL; - } - ScrStr *canonical_text = scr_str_new(canonical, strlen(canonical)); - text = scr_bytes_to_str(bytes, canonical_text); - scr_str_release(canonical_text); - } - scr_bytes_release(bytes); - if (!text) return NULL; - ScrDyn *result = scr_dyn_new_str(text); - scr_str_release(text); - return result; -} - /* getFormat: a falsy format is uncompressed; other values must be * 'compressed', 'hybrid' or 'uncompressed'. 0 after throwing. */ static int se_format(const ScrDyn *format) { if (!scr_dyn_truthy(format)) return SE_UNCOMPRESSED; if (format->kind == SCR_DYN_STR) { - if (se_str_is(format->v.str, "compressed")) return SE_COMPRESSED; - if (se_str_is(format->v.str, "hybrid")) return SE_HYBRID; - if (se_str_is(format->v.str, "uncompressed")) return SE_UNCOMPRESSED; + if (ca_str_is(format->v.str, "compressed")) return SE_COMPRESSED; + if (ca_str_is(format->v.str, "hybrid")) return SE_HYBRID; + if (ca_str_is(format->v.str, "uncompressed")) return SE_UNCOMPRESSED; } - se_throw_formatted("ERR_CRYPTO_ECDH_INVALID_FORMAT", "Invalid ECDH format: ", format); + ca_throw_formatted(SE_SUBJECT, "ERR_CRYPTO_ECDH_INVALID_FORMAT", "Invalid ECDH format: ", format); return 0; } @@ -516,7 +362,7 @@ static ScrDyn *se_new(const ScrDyn *curve) { } ScrDyn *scr_crypto_ecdh_new(ScrDyn *curve) { - ScrDyn *unboxed = se_unbox(curve); + ScrDyn *unboxed = ca_unbox(curve); if (!unboxed) return NULL; ScrDyn *result = se_new(unboxed); scr_dyn_release(unboxed); @@ -530,7 +376,7 @@ static ScrDyn *se_convert_key(const ScrDyn *key, const ScrDyn *curve, const ScrD scr_dyn_arg_type_fail("curve", "of type string", curve); return NULL; } - ScrBytes *bytes = se_key_bytes(key, in_enc); + ScrBytes *bytes = ca_key_bytes(SE_SUBJECT, key, in_enc); if (!bytes) return NULL; int form = se_format(format); if (!form) { @@ -557,7 +403,7 @@ static ScrDyn *se_convert_key(const ScrDyn *key, const ScrDyn *curve, const ScrD } else { ScrBytes *out = se_write_point(&grp, &pt, form); if (!out) se_operation_failed("Failed to convert EC_POINT to Buffer"); - else result = se_encode(out, out_enc); + else result = ca_encode(SE_SUBJECT, out, out_enc); } } mbedtls_ecp_point_free(&pt); @@ -571,7 +417,7 @@ ScrDyn *scr_crypto_ecdh_convert_key(ScrDyn *key, ScrDyn *curve, ScrDyn *in_enc, ScrDyn *unboxed[5] = {NULL, NULL, NULL, NULL, NULL}; ScrDyn *result = NULL; for (size_t i = 0; i < 5; i++) - if (!(unboxed[i] = se_unbox(args[i]))) goto done; + if (!(unboxed[i] = ca_unbox(args[i]))) goto done; result = se_convert_key(unboxed[0], unboxed[1], unboxed[2], unboxed[3], unboxed[4]); done: for (size_t i = 0; i < 5; i++) scr_dyn_release(unboxed[i]); @@ -597,7 +443,7 @@ static ScrDyn *se_get_public_key(ScEcdh *e, const ScrDyn *encoding, const ScrDyn se_operation_failed("Failed to get ECDH public key"); return NULL; } - return se_encode(out, encoding); + return ca_encode(SE_SUBJECT, out, encoding); } static ScrDyn *se_generate_keys(ScEcdh *e, const ScrDyn *encoding, const ScrDyn *format) { @@ -628,12 +474,12 @@ static ScrDyn *se_get_private_key(ScEcdh *e, const ScrDyn *encoding) { se_operation_failed("Failed to get ECDH private key"); return NULL; } - return se_encode(out, encoding); + return ca_encode(SE_SUBJECT, out, encoding); } /* setPrivateKey(key, encoding): 1 <= d < n, and the public key d·G. */ static ScrDyn *se_set_private_key(ScEcdh *e, ScrDyn *self, const ScrDyn *key, const ScrDyn *encoding) { - ScrBytes *bytes = se_key_bytes(key, encoding); + ScrBytes *bytes = ca_key_bytes(SE_SUBJECT, key, encoding); if (!bytes) return NULL; mbedtls_mpi d; mbedtls_ecp_point q; @@ -680,7 +526,7 @@ static void se_dep0031(void) { static ScrDyn *se_set_public_key(ScEcdh *e, ScrDyn *self, const ScrDyn *key, const ScrDyn *encoding) { se_dep0031(); - ScrBytes *bytes = se_key_bytes(key, encoding); + ScrBytes *bytes = ca_key_bytes(SE_SUBJECT, key, encoding); if (!bytes) return NULL; mbedtls_ecp_point q; mbedtls_ecp_point_init(&q); @@ -716,7 +562,7 @@ static bool se_pair_valid(ScEcdh *e) { /* computeSecret(key, inputEncoding, outputEncoding): the x coordinate of * d·P, padded to the field size as ECDH_compute_key writes it. */ static ScrDyn *se_compute_secret(ScEcdh *e, const ScrDyn *key, const ScrDyn *in_enc, const ScrDyn *out_enc) { - ScrBytes *bytes = se_key_bytes(key, in_enc); + ScrBytes *bytes = ca_key_bytes(SE_SUBJECT, key, in_enc); if (!bytes) return NULL; ScrDyn *result = NULL; mbedtls_ecp_point pub; @@ -737,7 +583,7 @@ static ScrDyn *se_compute_secret(ScEcdh *e, const ScrDyn *key, const ScrDyn *in_ scr_bytes_release(out); se_operation_failed("Failed to compute ECDH key"); } else { - result = se_encode(out, out_enc); + result = ca_encode(SE_SUBJECT, out, out_enc); } } mbedtls_mpi_free(&z); @@ -776,7 +622,7 @@ static ScrDyn *se_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *cons size_t count = argc < 3 ? argc : 3; ScrDyn *result = NULL; for (size_t i = 0; i < count; i++) - if (!(unboxed[i] = se_unbox(args[i]))) goto done; + if (!(unboxed[i] = ca_unbox(args[i]))) goto done; result = se_dispatch(h, self, method, unboxed, count, what); done: for (size_t i = 0; i < count; i++) scr_dyn_release(unboxed[i]); diff --git a/packages/runtime/test/dh.test.ts b/packages/runtime/test/dh.test.ts new file mode 100644 index 0000000000..d402d04658 --- /dev/null +++ b/packages/runtime/test/dh.test.ts @@ -0,0 +1,85 @@ +import { execFile } from "node:child_process"; +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { beforeAll, expect, test } from "vitest"; + +const execFileAsync = promisify(execFile); +const testDir = import.meta.dirname; +const bin = join(testDir, "build", "test_dh"); +const mbedtls = join(testDir, "../vendor/mbedtls"); + +const runtimeSources = [ + "scr_shared.c", + "scr_bytes.c", + "scr_bytes.test.c", + "scr_bytes_io.c", + "scr_fs_cp.c", + "scr_zlib.c", + "scr_lib.c", + "scr_hash.c", + "scr_string.c", + "scr_array.c", + "scr_map.c", + "scr_exception.c", + "scr_error.c", + "scr_number.c", + "scr_console.c", + "scr_closure.c", + "scr_ffi.c", + "scr_object.c", + "scr_union.c", + "scr_cycle.c", + "scr_alloc.c", + "scr_json.c", + "scr_inspect.c", + "scr_bigint.c", + "scr_async.c", + "scr_async_dyn.c", + "scr_child.c", + "scr_path.c", + "scr_url.c", + "scr_url_params.c", +]; + +// The bignum modules scr_dh.c uses. +const mbedtlsSources = ["bignum.c", "bignum_core.c", "constant_time.c", "platform_util.c"]; + +// Compiled once with ASan, UBSan and the RC audit. test_dh.c includes +// scr_dh.c and checks the primality tests' Montgomery exponentiation +// against mbedTLS's, the primality test on primes, Carmichael numbers and +// strong pseudoprimes, exponents past mbedTLS's 8192-bit limit, DH_check +// results, and generated safe primes. +beforeAll(async () => { + await mkdir(join(testDir, "build"), { recursive: true }); + await execFileAsync("clang", [ + "-std=c11", + "-O1", + "-Wall", + "-Wextra", + "-Wno-missing-field-initializers", + "-fsanitize=address,undefined", + "-DSCR_RC_AUDIT", + "-I", + join(testDir, "../src"), + "-I", + join(mbedtls, "include"), + "-I", + join(mbedtls, "library"), + "-o", + bin, + join(testDir, "test_dh.c"), + ...runtimeSources.map((source) => join(testDir, "../src", source)), + ...mbedtlsSources.map((source) => join(mbedtls, "library", source)), + "-Wno-deprecated-declarations", + "-lz", + ...(process.platform === "linux" ? ["-D_GNU_SOURCE", "-lm"] : []), + ]); +}, 300_000); + +test("DiffieHellman runtime: exponentiation, primality and safe primes", async () => { + const { stderr } = await execFileAsync(bin, [], { + env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" }, + }); + expect(stderr.trim().split("\n").pop()).toMatch(/^(\d+)\/\1 cases passed$/); +}, 300_000); diff --git a/packages/runtime/test/test_dh.c b/packages/runtime/test/test_dh.c new file mode 100644 index 0000000000..aa8f175a35 --- /dev/null +++ b/packages/runtime/test/test_dh.c @@ -0,0 +1,228 @@ +/* Unit tests for the DiffieHellman runtime's arithmetic (scr_dh.c), built + * with ASan and UBSan by dh.test.ts. The unit is included directly so the + * tests can compare the Montgomery exponentiation the primality tests use + * with mbedTLS's, run the primality test on primes, Carmichael numbers and + * strong pseudoprimes to small bases, split exponents past mbedTLS's limit, + * and check generated safe primes and DH_check results. */ +#include "../src/scr_dh.c" + +#include +#include + +static long total = 0, failed = 0; + +static void check(bool ok, const char *what) { + total++; + if (!ok) { + failed++; + fprintf(stderr, "FAIL: %s\n", what); + } +} + +static void from_hex(mbedtls_mpi *x, const char *hex) { + if (mbedtls_mpi_read_string(x, 16, hex) != 0) scr_trap("bad hex\n"); +} + +/* The Montgomery exponentiation, with the base 2 and with general bases, + * against mbedtls_mpi_exp_mod for moduli of one to 64 limbs. */ +static void test_mont_pow(void) { +#if SD_MONT + static const size_t sizes[] = {3, 64, 65, 127, 128, 129, 512, 1000, 2048, 4096}; + mbedtls_mpi n, base, e, two, got, want; + mbedtls_mpi_init(&n); + mbedtls_mpi_init(&base); + mbedtls_mpi_init(&e); + mbedtls_mpi_init(&two); + mbedtls_mpi_init(&got); + mbedtls_mpi_init(&want); + mbedtls_mpi_lset(&two, 2); + for (size_t s = 0; s < sizeof sizes / sizeof sizes[0]; s++) { + for (int trial = 0; trial < 6; trial++) { + size_t bits = sizes[s]; + sd_random_bits(&n, bits, true); + if (mbedtls_mpi_cmp_int(&n, 3) <= 0) mbedtls_mpi_lset(&n, 7); + sd_random_bits(&base, bits > 2 ? bits - 1 : 1, false); + size_t ebits = trial == 0 ? 1 : trial == 1 ? bits + 70 : (bits * (size_t)trial) / 5 + 1; + sd_random_bits(&e, ebits, false); + if (trial == 0) mbedtls_mpi_lset(&e, 0); + ScDhMont m; + bool ready = sd_mont_init(&m, &n); + check(ready, "sd_mont_init"); + char what[96]; + snprintf(what, sizeof what, "base^e mod n for %zu bits, trial %d", bits, trial); + bool ok = ready && sd_mont_pow(&m, &got, &base, &e) && mbedtls_mpi_exp_mod(&want, &base, &e, &n, NULL) == 0; + check(ok && mbedtls_mpi_cmp_mpi(&got, &want) == 0, what); + snprintf(what, sizeof what, "2^e mod n for %zu bits, trial %d", bits, trial); + ok = ready && sd_mont_pow(&m, &got, NULL, &e) && mbedtls_mpi_exp_mod(&want, &two, &e, &n, NULL) == 0; + check(ok && mbedtls_mpi_cmp_mpi(&got, &want) == 0, what); + sd_mont_free(&m); + } + } + mbedtls_mpi_free(&n); + mbedtls_mpi_free(&base); + mbedtls_mpi_free(&e); + mbedtls_mpi_free(&two); + mbedtls_mpi_free(&got); + mbedtls_mpi_free(&want); +#endif +} + +/* BN_check_prime's answers for small values, Mersenne numbers, Carmichael + * numbers and strong pseudoprimes to small prime bases. */ +static void test_is_prime(void) { + static const struct { + const char *hex; + bool prime; + } cases[] = { + {"0", false}, + {"1", false}, + {"2", true}, + {"3", true}, + {"4", false}, + {"5", true}, + {"9", false}, + {"231", false}, /* 561, a Carmichael number */ + {"451", false}, /* 1105, a Carmichael number */ + {"7ff", false}, /* 2047, a strong pseudoprime to the base 2 */ + {"45c5", false}, /* 17861 = 53 · 337 */ + {"45c7", true}, /* 17863, the largest trial-division prime */ + {"bfa17dc7", false}, /* 3215031751, to the bases 2, 3, 5 and 7 */ + {"351591274f9af9fb", false}, /* 3825123056546413051, to the bases up to 23 */ + {"437ae92817f9fc85b7e5", false}, /* to the prime bases up to 37 */ + {"2be6951adc5b22410a5fd", false}, /* to the prime bases up to 41 */ + {"1fffffffffffffff", true}, /* 2^61 - 1 */ + {"1ffffffffffffffffffffff", true}, /* 2^89 - 1 */ + {"7fffffffffffffffffffffffffffffff", true}, /* 2^127 - 1 */ + {"100000000000000000000000000000001", false}, /* 2^128 + 1 */ + }; + ScDhSmallPrimes *small = sd_small_primes(); + mbedtls_mpi x; + mbedtls_mpi_init(&x); + for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) { + from_hex(&x, cases[i].hex); + char what[96]; + snprintf(what, sizeof what, "is_prime(0x%s) == %d", cases[i].hex, cases[i].prime); + check(sd_is_prime(small, &x) == cases[i].prime, what); + } + /* The modp groups' primes are safe primes. */ + for (size_t i = 0; i < 4; i++) { + mbedtls_mpi_read_binary(&x, sd_groups[i].p, sd_groups[i].p_len); + check(sd_is_prime(small, &x), "a modp prime is prime"); + mbedtls_mpi_shift_r(&x, 1); + check(sd_is_prime(small, &x), "half a modp prime is prime"); + mbedtls_mpi_add_int(&x, &x, 2); + check(!sd_is_prime(small, &x), "half a modp prime plus 2 is composite"); + } + mbedtls_mpi_free(&x); + free(small); +} + +/* DH_check bits for small and composite primes, the Pocklington path for + * safe primes, and a generator at p - 1. */ +static void test_check_params(void) { + static const struct { + const char *p; + int g; + int codes; + } cases[] = { + {"0", 2, 137}, {"1", 2, 137}, {"2", 2, 139}, {"3", 2, 138}, {"5", 2, 128}, {"5", 5, 136}, + {"7", 2, 128}, {"9", 2, 129}, {"f", 2, 129}, {"11", 2, 130}, {"17", 2, 128}, {"17", 22, 136}, + {"18", 2, 129}, {"231", 2, 129}, {"7ff", 2, 129}, + }; + mbedtls_mpi p, g; + mbedtls_mpi_init(&p); + mbedtls_mpi_init(&g); + for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) { + from_hex(&p, cases[i].p); + mbedtls_mpi_lset(&g, cases[i].g); + char what[96]; + int codes = sd_check_params(&p, &g); + snprintf(what, sizeof what, "check(0x%s, %d) == %d (got %d)", cases[i].p, cases[i].g, cases[i].codes, codes); + check(codes == cases[i].codes, what); + } + mbedtls_mpi_read_binary(&p, sd_groups[1].p, sd_groups[1].p_len); + mbedtls_mpi_lset(&g, 2); + check(sd_check_params(&p, &g) == 0, "check(modp2, 2) == 0"); + mbedtls_mpi_sub_int(&g, &p, 1); + check(sd_check_params(&p, &g) == SD_NOT_SUITABLE_GENERATOR, "check(modp2, p - 1) == 8"); + mbedtls_mpi_lset(&g, 2); + mbedtls_mpi_add_int(&p, &p, 2); + check(sd_check_params(&p, &g) == SD_P_NOT_PRIME, "check(modp2 + 2, 2) == 1"); + mbedtls_mpi_free(&p); + mbedtls_mpi_free(&g); +} + +/* Exponents past 8192 bits, split into digits, against the exponent + * reduced modulo p - 1 (Fermat) for a prime p. */ +static void test_long_exponent(void) { + mbedtls_mpi p, p1, base, e, reduced, got, want; + mbedtls_mpi_init(&p); + mbedtls_mpi_init(&p1); + mbedtls_mpi_init(&base); + mbedtls_mpi_init(&e); + mbedtls_mpi_init(&reduced); + mbedtls_mpi_init(&got); + mbedtls_mpi_init(&want); + mbedtls_mpi_read_binary(&p, sd_groups[0].p, sd_groups[0].p_len); + mbedtls_mpi_sub_int(&p1, &p, 1); + mbedtls_mpi_lset(&base, 5); + static const size_t sizes[] = {8192, 8193, 16382, 16383, 20000}; + for (size_t i = 0; i < sizeof sizes / sizeof sizes[0]; i++) { + sd_random_bits(&e, sizes[i], false); + mbedtls_mpi_mod_mpi(&reduced, &e, &p1); + bool ok = sd_exp(&got, &base, &e, &p) && mbedtls_mpi_exp_mod(&want, &base, &reduced, &p, NULL) == 0; + char what[64]; + snprintf(what, sizeof what, "5^e mod p for a %zu-bit e", sizes[i]); + check(ok && mbedtls_mpi_cmp_mpi(&got, &want) == 0, what); + } + mbedtls_mpi_lset(&p, 24); + check(!sd_exp(&got, &base, &e, &p), "an even modulus fails"); + mbedtls_mpi_lset(&p, 1); + check(!sd_exp(&got, &base, &e, &p), "the modulus 1 fails"); + mbedtls_mpi_free(&p); + mbedtls_mpi_free(&p1); + mbedtls_mpi_free(&base); + mbedtls_mpi_free(&e); + mbedtls_mpi_free(&reduced); + mbedtls_mpi_free(&got); + mbedtls_mpi_free(&want); +} + +/* Generated safe primes have the requested length and congruence, and + * mbedTLS's own primality test accepts p and (p - 1) / 2. */ +static void test_generate(void) { + static const unsigned adds[] = {24, 60, 12}; + mbedtls_mpi p, q, g; + mbedtls_mpi_init(&p); + mbedtls_mpi_init(&q); + mbedtls_mpi_init(&g); + 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); + 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); + 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; + char what[64]; + snprintf(what, sizeof what, "a 512-bit safe prime ≡ %u (mod %u)", adds[i] - 1, adds[i]); + check(ok, what); + check(sd_check_params(&p, &g) == 0, "a generated prime has no DH_check findings"); + } + } + mbedtls_mpi_free(&p); + mbedtls_mpi_free(&q); + mbedtls_mpi_free(&g); +} + +int main(void) { + test_mont_pow(); + test_is_prime(); + test_check_params(); + test_long_exponent(); + test_generate(); + fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total); + return failed ? 1 : 0; +} diff --git a/tests/corpus/6706-dh-groups.ts b/tests/corpus/6706-dh-groups.ts new file mode 100644 index 0000000000..24c8b57731 --- /dev/null +++ b/tests/corpus/6706-dh-groups.ts @@ -0,0 +1,52 @@ +// crypto.getDiffieHellman and createDiffieHellmanGroup on every modp +// group: the RFC primes and generator, verifyError, case-insensitive names, +// private keys of the group's width, and key pairs that agree on one +// secret of the prime's length. +import { createDiffieHellmanGroup, getDiffieHellman, type DiffieHellmanGroup } from "node:crypto"; + +const groups = ["modp1", "modp2", "modp5", "modp14", "modp15", "modp16", "modp17", "modp18"]; + +function bitLength(bytes: Buffer): number { + if (bytes.length === 0) return 0; + return (bytes.length - 1) * 8 + bytes[0]!.toString(2).length; +} + +for (const name of groups) { + const group: DiffieHellmanGroup = getDiffieHellman(name); + const prime: Buffer = group.getPrime(); + console.log( + name, + group.verifyError, + bitLength(prime), + prime.toString("hex").slice(0, 24), + prime.toString("hex").slice(-16), + group.getGenerator("hex"), + ); +} + +// Key exchange on the groups whose exponentiations stay quick. +for (const name of ["modp1", "modp2", "modp5", "modp14"]) { + const alice = getDiffieHellman(name); + const bob = createDiffieHellmanGroup(name.toUpperCase()); + const alicePublic: Buffer = alice.generateKeys(); + const bobPublic: string = bob.generateKeys("base64"); + const fromAlice: Buffer = alice.computeSecret(bobPublic, "base64"); + const fromBob: string = bob.computeSecret(alicePublic, null, "hex"); + const width = bitLength(alice.getPrivateKey()); + console.log( + name, + fromAlice.length, + fromAlice.toString("hex") === fromBob, + alicePublic.length <= fromAlice.length, + name === "modp1" || name === "modp2" ? width <= bitLength(alice.getPrime()) : width, + ); + // generateKeys keeps the key pair it has. + console.log(alice.generateKeys("hex") === alicePublic.toString("hex"), alice.getPublicKey().equals(alicePublic)); +} + +// The prime and generator in every encoding. +const modp5 = getDiffieHellman("Modp5"); +for (const encoding of ["hex", "base64", "base64url", "latin1", "binary"] as const) { + const prime: string = modp5.getPrime(encoding as "hex"); + console.log(encoding, prime.length, JSON.stringify(modp5.getGenerator(encoding as "hex"))); +} diff --git a/tests/corpus/6707-dh-explicit-params.ts b/tests/corpus/6707-dh-explicit-params.ts new file mode 100644 index 0000000000..57e239d731 --- /dev/null +++ b/tests/corpus/6707-dh-explicit-params.ts @@ -0,0 +1,169 @@ +// crypto.createDiffieHellman over explicit parameters: fixed private keys +// give Node's public keys and secrets, verifyError reports DH_check's +// findings, and parameters that equal an OpenSSL named group draw private +// keys of the group's length. +import { createDiffieHellman, getDiffieHellman, type DiffieHellman } from "node:crypto"; + +// A 512-bit safe prime with p ≡ 23 (mod 24). +const P512 = + "f29864297728009aab9d4220c05ca2d80a5e4ac2f016cd098543a2486bde8bcb" + + "deed9eab7029c8d2b7a681f9495bfc7b1c39c6db64457bb79beeb74a4b78f5ff"; + +function bitLength(bytes: Buffer): number { + if (bytes.length === 0) return 0; + return (bytes.length - 1) * 8 + bytes[0]!.toString(2).length; +} + +function fixed(prime: Buffer, generator: number | Buffer, key: string): DiffieHellman { + const dh = createDiffieHellman(prime, generator); + dh.setPrivateKey(key, "hex"); + return dh; +} + +const prime512 = Buffer.from(P512, "hex"); +for (const generator of [2, 3, 5, Buffer.from([0x0b])]) { + const alice = fixed(prime512, generator, "0123456789abcdef0123456789abcdef"); + const bob = fixed(prime512, generator, "fedcba9876543210fedcba9876543210ff"); + const alicePublic: string = alice.generateKeys("hex"); + const bobPublic: Buffer = bob.generateKeys(); + const secret: string = alice.computeSecret(bobPublic, null, "hex"); + console.log(alice.getGenerator("hex"), alice.verifyError, alicePublic.slice(0, 32), bobPublic.length); + console.log(secret, secret === bob.computeSecret(alicePublic, "hex", "hex")); +} + +// Secrets pad to the prime's length; public keys are minimal. +const modp1 = getDiffieHellman("modp1").getPrime(); +const small = fixed(modp1, 2, "01"); +console.log(small.generateKeys("hex"), small.getPrivateKey("hex")); +const big = fixed(modp1, 2, "ff".repeat(100)); +console.log(big.generateKeys().length, big.getPrivateKey().length); +const padded: Buffer = small.computeSecret(Buffer.from([4])); +console.log(padded.length, padded.subarray(0, 4).toString("hex"), padded.subarray(-2).toString("hex")); +console.log(big.computeSecret(Buffer.from([2]), null, "base64url")); + +// A private key of 0 still yields a public key; reading it back and +// deriving a secret from it fail. A private key of 1 answers the peer's +// key. +function report(run: () => unknown): void { + try { + const result = run(); + console.log(Buffer.isBuffer(result) ? result.toString("hex") : result); + } catch (error) { + const e = error as Error & { code?: string }; + console.log(e.name, e.code, e.message); + } +} +const zero = fixed(prime512, 2, "00"); +console.log(zero.generateKeys("hex")); +report(() => zero.getPrivateKey()); +report(() => zero.computeSecret(Buffer.from([5]))); +report(() => fixed(prime512, 2, "01").computeSecret(Buffer.from([5]))); + +// A private key longer than the prime: 8800 bits. +const long = fixed(modp1, 2, "ff".repeat(1100)); +console.log(long.generateKeys("hex").slice(0, 32), long.computeSecret(Buffer.from([7]), null, "hex").slice(-32)); + +// setPublicKey and setPrivateKey answer the object and replace the keys. +const keys = createDiffieHellman(prime512); +keys.setPublicKey(Buffer.from([0, 0, 7])); +keys.setPrivateKey("0005", "hex"); +console.log(keys.getPublicKey("hex"), keys.getPrivateKey("hex")); +console.log(keys.generateKeys("hex").length, keys.getPublicKey("hex") !== "07"); + +// verifyError for small, even, composite and non-safe primes and for +// unsuitable generators. +const cases: [string, number | string][] = [ + ["01", 2], + ["02", 2], + ["03", 2], + ["0d", 2], + ["15", 2], + ["17", 2], + ["17", 5], + ["17", "16"], + ["17", "1e"], + ["18", 2], + ["000017", 2], + ["05", 2], + ["05", 5], + ["07", 2], + ["09", 2], + ["0b", 5], + ["0f", 2], + ["11", 2], + ["2f", 2], + ["3b", 5], + ["0231", 2], + ["0451", 2], + ["07ff", 2], +]; +for (const [prime, generator] of cases) { + const g = typeof generator === "number" ? generator : Buffer.from(generator, "hex"); + console.log(prime, generator, createDiffieHellman(Buffer.from(prime, "hex"), g).verifyError); +} +const almost = Buffer.from(P512, "hex"); +almost[almost.length - 1] = 0xfe; +console.log(createDiffieHellman(almost).verifyError); +const composite = Buffer.from(P512, "hex"); +composite[composite.length - 1] = 0xfd; +console.log(createDiffieHellman(composite).verifyError); +const generatorPMinus1 = Buffer.from(P512, "hex"); +generatorPMinus1[generatorPMinus1.length - 1] = 0xfe; +console.log(createDiffieHellman(prime512, generatorPMinus1).verifyError); + +// Generated keys: bits(p) - 2 bits for other primes, at most the named +// group's key length for RFC 3526 and RFC 5114 parameters. +const generated = createDiffieHellman(prime512); +generated.generateKeys(); +console.log(bitLength(generated.getPrivateKey())); +const modp14 = getDiffieHellman("modp14").getPrime(); +const named = createDiffieHellman(modp14, Buffer.from([0, 2])); +named.generateKeys(); +console.log(named.verifyError, bitLength(named.getPrivateKey()) <= 225); +const notNamed = createDiffieHellman(modp1); +notNamed.generateKeys(); +console.log(bitLength(notNamed.getPrivateKey())); +const rfc5114 = createDiffieHellman( + Buffer.from( + "b10b8f96a080e01dde92de5eae5d54ec52c99fbcfb06a3c69a6a9dca52d23b616073e28675a23d189838ef1e2ee652c0" + + "13ecb4aea906112324975c3cd49b83bfaccbdd7d90c4bd7098488e9c219a73724effd6fae5644738faa31a4ff55bccc0" + + "a151af5f0dc8b4bd45bf37df365c1a65e68cfda76d4da708df1fb2bc2e4a4371", + "hex", + ), + Buffer.from( + "a4d1cbd5c3fd34126765a442efb99905f8104dd258ac507fd6406cff14266d31266fea1e5c41564b777e690f5504f213" + + "160217b4b01b886a5e91547f9e2749f4d7fbd7d3b9a92ee1909d0d2263f80a76a6a24c087a091f531dbf0a0169b6a28a" + + "d662a4d18e73afa32d779d5918d08bc8858f4dcef97c2a24855e6eeb22b3b2e5", + "hex", + ), +); +rfc5114.generateKeys(); +console.log(rfc5114.verifyError, bitLength(rfc5114.getPrivateKey()) <= 160); +// RFC 5114's 2048-bit group with a 256-bit subgroup draws 224-bit keys. +const rfc5114q256 = createDiffieHellman( + Buffer.from( + "87a8e61db4b6663cffbbd19c651959998ceef608660dd0f25d2ceed4435e3b00e00df8f1d61957d4faf7df4561b2aa30" + + "16c3d91134096faa3bf4296d830e9a7c209e0c6497517abd5a8a9d306bcf67ed91f9e6725b4758c022e0b1ef4275bf7b" + + "6c5bfc11d45f9088b941f54eb1e59bb8bc39a0bf12307f5c4fdb70c581b23f76b63acae1caa6b7902d52526735488a0e" + + "f13c6d9a51bfa4ab3ad8347796524d8ef6a167b5a41825d967e144e5140564251ccacb83e6b486f6b3ca3f7971506026" + + "c0b857f689962856ded4010abd0be621c3a3960a54e710c375f26375d7014103a4b54330c198af126116d2276e11715f" + + "693877fad7ef09cadb094ae91e1a1597", + "hex", + ), + Buffer.from( + "3fb32c9b73134d0b2e77506660edbd484ca7b18f21ef205407f4793a1a0ba12510dbc15077be463fff4fed4aac0bb555" + + "be3a6c1b0c6b47b1bc3773bf7e8c6f62901228f8c28cbb18a55ae31341000a650196f931c77a57f2ddf463e5e9ec144b" + + "777de62aaab8a8628ac376d282d6ed3864e67982428ebc831d14348f6f2f9193b5045af2767164e1dfc967c1fb3f2e55" + + "a4bd1bffe83b9c80d052b985d182ea0adb2a3b7313d3fe14c8484b1e052588b9b7d2bbd2df016199ecd06e1557cd0915" + + "b3353bbb64e0ec377fd028370df92b52c7891428cdc67eb6184b523d1db246c32f63078490f00ef8d647d148d4795451" + + "5e2327cfef98c582664b4c0f6cc41659", + "hex", + ), +); +let widest = 0; +for (let i = 0; i < 8; i++) { + const keys = createDiffieHellman(rfc5114q256.getPrime(), rfc5114q256.getGenerator()); + keys.generateKeys(); + widest = Math.max(widest, bitLength(keys.getPrivateKey())); +} +console.log(rfc5114q256.verifyError, widest <= 224, widest > 200); diff --git a/tests/corpus/6708-dh-errors.js b/tests/corpus/6708-dh-errors.js new file mode 100644 index 0000000000..e092da7277 --- /dev/null +++ b/tests/corpus/6708-dh-errors.js @@ -0,0 +1,147 @@ +// createDiffieHellman, getDiffieHellman and DiffieHellman errors from +// JavaScript: argument types and ranges, the keyEncoding and generator +// forms, OpenSSL's generator and modulus errors with their library and +// reason properties, parameter sizes, group names, encodings, keys read +// before they exist, public keys outside (1, p - 1), key generation and +// secrets on primes outside 512 to 10000 bits, and members a +// DiffieHellmanGroup does not have. +import crypto from "node:crypto"; + +// Values the declared overloads reject reach the calls through an +// untyped function, as they would from other untyped code. +const loose = (value) => value; + +function attempt(label, run) { + try { + const result = run(); + if (Buffer.isBuffer(result)) console.log(label, "ok", result.toString("hex")); + else if (typeof result === "object" && result !== null) + console.log(label, "ok", result.getGenerator("hex"), result.getPrime().length); + else console.log(label, "ok", JSON.stringify(result)); + } catch (e) { + const extra = e.library === undefined ? "" : ` [${Object.keys(e).join(",")}] ${e.library} / ${e.reason}`; + console.log(label, e.name, e.code, e.message + extra); + } +} + +const prime = crypto.getDiffieHellman("modp1").getPrime(); + +attempt("size missing", () => crypto.createDiffieHellman(loose(undefined))); +attempt("size null", () => crypto.createDiffieHellman(loose(null))); +attempt("size boolean", () => crypto.createDiffieHellman(loose(true))); +attempt("size object", () => crypto.createDiffieHellman(loose({}))); +attempt("size bigint", () => crypto.createDiffieHellman(loose(5n))); +attempt("size fraction", () => crypto.createDiffieHellman(1.5)); +attempt("size NaN", () => crypto.createDiffieHellman(NaN)); +attempt("size 2^31", () => crypto.createDiffieHellman(2 ** 31)); +attempt("size 2^40", () => crypto.createDiffieHellman(2 ** 40)); +attempt("size -1", () => crypto.createDiffieHellman(-1)); +attempt("size -0", () => crypto.createDiffieHellman(-0)); +attempt("size 1", () => crypto.createDiffieHellman(1)); +attempt("size 1 bad generator", () => crypto.createDiffieHellman(1, 1)); +attempt("size 2 generator 1", () => crypto.createDiffieHellman(2, 1)); +attempt("size 2", () => crypto.createDiffieHellman(2)); +attempt("size 511", () => crypto.createDiffieHellman(511)); +attempt("size 10001", () => crypto.createDiffieHellman(10001)); +attempt("size generator string", () => crypto.createDiffieHellman(64, loose("02"))); +attempt("size generator view", () => crypto.createDiffieHellman(64, loose(Buffer.from([2])))); + +attempt("generator 0", () => crypto.createDiffieHellman(prime, 0)); +attempt("generator 1", () => crypto.createDiffieHellman(prime, 1)); +attempt("generator -1", () => crypto.createDiffieHellman(prime, -1)); +attempt("generator fraction", () => crypto.createDiffieHellman(prime, 2.5)); +attempt("generator 2^31", () => crypto.createDiffieHellman(prime, 2 ** 31)); +attempt("generator NaN", () => crypto.createDiffieHellman(prime, NaN)); +attempt("generator null", () => crypto.createDiffieHellman(prime, loose(null))); +attempt("generator empty string", () => crypto.createDiffieHellman(prime, loose(""))); +attempt("generator true", () => crypto.createDiffieHellman(prime, loose(true))); +attempt("generator object", () => crypto.createDiffieHellman(prime, loose({}))); +attempt("generator view 1", () => crypto.createDiffieHellman(prime, Buffer.from([1]))); +attempt("generator view 0", () => crypto.createDiffieHellman(prime, Buffer.from([0]))); +attempt("generator view empty", () => crypto.createDiffieHellman(prime, Buffer.alloc(0))); +attempt("generator view 0002", () => crypto.createDiffieHellman(prime, Buffer.from([0, 2]))); +attempt("generator = p", () => crypto.createDiffieHellman(prime, prime)); +attempt("generator > p", () => crypto.createDiffieHellman(prime, Buffer.concat([Buffer.from([1]), prime]))); +attempt("generator string", () => crypto.createDiffieHellman(prime, loose("abc"))); +attempt("generator string hex", () => crypto.createDiffieHellman(prime, loose("05"), loose("hex"))); +attempt("generator bad encoding", () => crypto.createDiffieHellman(prime, loose("05"), loose("nope"))); +attempt("generator uint16", () => crypto.createDiffieHellman(prime, new Uint16Array([0x0300]))); + +attempt("prime hex", () => crypto.createDiffieHellman(prime.toString("hex"), "hex")); +attempt("prime base64 generator", () => crypto.createDiffieHellman(prime.toString("base64"), "base64", 5)); +attempt("prime utf8", () => crypto.createDiffieHellman("abc").verifyError); +attempt("prime buffer encoding", () => crypto.createDiffieHellman("abc", "buffer")); +attempt("keyEncoding as generator", () => crypto.createDiffieHellman("abc", loose("nope")).verifyError); +attempt("keyEncoding as generator with encoding", () => crypto.createDiffieHellman("abc", loose("nope"), loose("hex"))); +attempt("keyEncoding number", () => crypto.createDiffieHellman(prime, loose(5))); +attempt("keyEncoding on a view", () => crypto.createDiffieHellman(prime, loose("hex"), 5)); +attempt("prime ArrayBuffer", () => crypto.createDiffieHellman(new Uint8Array(prime).buffer)); +attempt("prime DataView", () => crypto.createDiffieHellman(new DataView(new ArrayBuffer(3))).verifyError); +attempt("prime zero", () => crypto.createDiffieHellman(Buffer.alloc(4)).getPrime()); + +attempt("group unknown", () => crypto.getDiffieHellman("modp3")); +attempt("group empty", () => crypto.getDiffieHellman("")); +attempt("group ffdhe", () => crypto.createDiffieHellmanGroup("ffdhe2048")); +attempt("group case", () => crypto.getDiffieHellman("MoDp2")); +attempt("group missing", () => crypto.getDiffieHellman(loose(undefined))); +attempt("group number", () => crypto.getDiffieHellman(loose(14))); + +const group = crypto.getDiffieHellman("modp2"); +attempt("group public before keys", () => group.getPublicKey()); +attempt("group private before keys", () => group.getPrivateKey()); +attempt("group secret before keys", () => group.computeSecret(Buffer.from([5]))); +const untypedGroup = loose(group); +attempt("group setPublicKey", () => untypedGroup.setPublicKey(Buffer.from([5]))); +attempt("group setPrivateKey", () => untypedGroup.setPrivateKey(Buffer.from([5]))); +group.generateKeys(); +attempt("secret 0", () => group.computeSecret(Buffer.from([0]))); +attempt("secret empty", () => group.computeSecret(Buffer.alloc(0))); +attempt("secret 1", () => group.computeSecret(Buffer.from([0, 1]))); +attempt("secret p - 1", () => { + const key = Buffer.from(group.getPrime()); + key[key.length - 1]--; + return group.computeSecret(key); +}); +attempt("secret p", () => group.computeSecret(group.getPrime())); +attempt("secret 2", () => group.computeSecret(Buffer.from([2])).length); +attempt("secret number", () => group.computeSecret(loose(5))); +attempt("secret bad input encoding", () => group.computeSecret("ab", loose("nope"))); +attempt("secret bad output encoding", () => untypedGroup.computeSecret(Buffer.from([2]), null, "nope")); +attempt("getPrime bad encoding", () => untypedGroup.getPrime("nope")); +attempt("getPrime HEX", () => untypedGroup.getPrime("HEX").slice(0, 8)); +attempt("getGenerator utf16le", () => untypedGroup.getGenerator("utf16le")); +attempt("verifyError call", () => untypedGroup.verifyError()); +attempt("unknown member", () => untypedGroup.bogus()); + +const fresh = crypto.createDiffieHellman(prime); +attempt("public before keys", () => fresh.getPublicKey()); +attempt("private before keys", () => fresh.getPrivateKey()); +attempt("secret before keys", () => fresh.computeSecret(Buffer.from([5]))); +attempt("setPublicKey number", () => fresh.setPublicKey(loose(5))); +attempt("setPrivateKey bad encoding", () => fresh.setPrivateKey("ab", loose("nope"))); +attempt("setPublicKey 0", () => loose(fresh.setPublicKey(Buffer.from([0]))).getPublicKey()); +attempt("setPrivateKey utf8", () => loose(fresh.setPrivateKey("ab")).getPrivateKey("hex")); + +// Primes outside 512 to 10000 bits: key generation and secrets fail, but a +// private key the program sets still yields a public key. +for (const value of [23, 1, 0, 24]) { + const label = `p ${value}`; + const dh = crypto.createDiffieHellman(Buffer.from([value])); + attempt(`${label} generateKeys`, () => dh.generateKeys()); + attempt(`${label} secret 2`, () => dh.computeSecret(Buffer.from([2]))); + attempt(`${label} secret 5`, () => dh.computeSecret(Buffer.from([5]))); + dh.setPrivateKey(Buffer.from([3])); + attempt(`${label} generateKeys with key`, () => dh.generateKeys()); + attempt(`${label} public with key`, () => dh.getPublicKey()); + attempt(`${label} secret with key`, () => dh.computeSecret(Buffer.from([5]))); +} +const even = Buffer.from(prime); +even[even.length - 1] = 0xfe; +const evenDh = crypto.createDiffieHellman(even); +attempt("even generateKeys", () => evenDh.generateKeys()); +evenDh.setPrivateKey(Buffer.from([3])); +attempt("even generateKeys with key", () => evenDh.generateKeys()); +attempt("p 23 generator 30", () => + loose(crypto.createDiffieHellman(Buffer.from([23]), 30).setPrivateKey(Buffer.from([2]))).generateKeys(), +); +attempt("p 9", () => loose(crypto.createDiffieHellman(Buffer.from([9])).setPrivateKey(Buffer.from([3]))).generateKeys()); diff --git a/tests/corpus/6709-dh-generate.ts b/tests/corpus/6709-dh-generate.ts new file mode 100644 index 0000000000..b041605c26 --- /dev/null +++ b/tests/corpus/6709-dh-generate.ts @@ -0,0 +1,47 @@ +// crypto.createDiffieHellman(primeLength, generator) generates a safe +// prime of the requested length: p ≡ 23 (mod 24) for the generator 2, +// p ≡ 59 (mod 60) for 5 and p ≡ 11 (mod 12) otherwise. A second party built +// from the first one's prime and generator agrees on the secret. +import { createDiffieHellman } from "node:crypto"; + +function bitLength(bytes: Buffer): number { + if (bytes.length === 0) return 0; + return (bytes.length - 1) * 8 + bytes[0]!.toString(2).length; +} + +function mod(bytes: Buffer, m: number): number { + let r = 0; + for (const byte of bytes) r = (r * 256 + byte) % m; + return r; +} + +for (const generator of [undefined, 2, 5, 3, 7]) { + const alice = generator === undefined ? createDiffieHellman(512) : createDiffieHellman(512, generator); + const prime = alice.getPrime(); + console.log( + generator, + bitLength(prime), + alice.getGenerator("hex"), + alice.verifyError, + generator === undefined || generator === 2 + ? mod(prime, 24) + : generator === 5 + ? mod(prime, 60) + : mod(prime, 12), + ); + const bob = createDiffieHellman(alice.getPrime(), alice.getGenerator()); + const alicePublic = alice.generateKeys(); + const bobPublic = bob.generateKeys("hex"); + const secret = alice.computeSecret(bobPublic, "hex"); + console.log( + bob.verifyError, + bitLength(alice.getPrivateKey()), + secret.length, + secret.equals(bob.computeSecret(alicePublic)), + ); +} + +// A string prime with its encoding, and the generator as a string. +const shared = createDiffieHellman(512); +const copy = createDiffieHellman(shared.getPrime("base64"), "base64", shared.getGenerator("hex"), "hex"); +console.log(copy.getPrime().equals(shared.getPrime()), copy.getGenerator("hex"), copy.verifyError); diff --git a/tests/corpus/6710-dh-encodings.cjs b/tests/corpus/6710-dh-encodings.cjs new file mode 100644 index 0000000000..100ee95e27 --- /dev/null +++ b/tests/corpus/6710-dh-encodings.cjs @@ -0,0 +1,73 @@ +// DiffieHellman string inputs and outputs from CommonJS: primes and +// generators as strings in every binary encoding, the keyEncoding slot +// read as the generator, keys set in every Buffer encoding and read in the +// declared ones, and computeSecret's input and output encodings. +const crypto = require("node:crypto"); + +// Values the declared overloads do not cover reach the calls through an +// untyped function, as they would from other untyped code. +const loose = (value) => value; + +const prime = crypto.getDiffieHellman("modp2").getPrime(); +const encodings = ["hex", "base64", "base64url", "latin1", "binary", "HEX", "Base64"]; + +for (const encoding of encodings) { + const text = prime.toString(encoding.toLowerCase()); + const dh = crypto.createDiffieHellman(text, encoding, "05", "hex"); + console.log(encoding, dh.getPrime().equals(prime), dh.getGenerator("hex")); +} +console.log(crypto.createDiffieHellman(prime.toString("hex"), "hex", "05", "hex").verifyError); + +// A truthy keyEncoding that names no encoding is the generator, and the +// generator slot its encoding. +console.log(crypto.createDiffieHellman(prime, 7).getGenerator("hex")); +console.log(crypto.createDiffieHellman(prime, "\x05").getGenerator("hex")); +console.log(crypto.createDiffieHellman(prime, Buffer.from([3])).getGenerator("hex")); +console.log(crypto.createDiffieHellman(prime, "buffer", 11).getGenerator("hex")); +console.log(crypto.createDiffieHellman(prime, "0d", "hex").getGenerator("hex")); +console.log(crypto.createDiffieHellman(prime, "base64", "Ew==", "base64").getGenerator("hex")); +console.log(crypto.createDiffieHellman(prime, "", 0, "").getGenerator("hex")); + +// Keys in and out of every encoding. +const alice = crypto.createDiffieHellman(prime); +const bob = crypto.createDiffieHellman(prime); +alice.setPrivateKey("q83vEjRWeJA=", "base64"); +bob.setPrivateKey(Buffer.from("10325476", "hex")); +alice.generateKeys(); +bob.generateKeys(); +function report(encoding, key, secret) { + const bytes = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, encoding); + console.log(encoding, typeof key, key.length, bytes.toString("hex").slice(0, 24)); +} +const hex = alice.getPublicKey("hex"); +report("hex", hex, bob.computeSecret(hex, "hex", "hex")); +const base64 = alice.getPublicKey("base64"); +report("base64", base64, bob.computeSecret(base64, "base64", "base64")); +const base64url = alice.getPublicKey("base64url"); +report("base64url", base64url, bob.computeSecret(base64url, "base64url", "base64url")); +const binary = alice.getPublicKey("binary"); +report("binary", binary, bob.computeSecret(binary, "binary", "binary")); +const view = alice.getPublicKey(); +report("undefined", view, bob.computeSecret(view, undefined, undefined)); +report("null", view, bob.computeSecret(view, null, null)); +// Encodings @types/node does not declare for these methods reach them +// through an untyped receiver. +const untypedBob = loose(bob); +console.log(alice.getPrivateKey("hex"), alice.getPrivateKey("base64url"), untypedBob.getPrivateKey("latin1").length); +console.log(untypedBob.getPublicKey("utf16le").length, untypedBob.getPrime("ucs2").length, untypedBob.getGenerator("buffer")); +console.log(untypedBob.computeSecret(loose(alice).getPublicKey("latin1"), "latin1", "base64").slice(0, 16)); + +// setPublicKey and setPrivateKey read strings through their encoding; +// 'buffer' and a missing encoding read utf8. +const keys = crypto.createDiffieHellman(prime); +keys.setPublicKey("ffee", "hex"); +loose(keys).setPrivateKey("zz", "buffer"); +console.log(keys.getPublicKey("hex"), keys.getPrivateKey("hex")); +keys.setPublicKey("é"); +console.log(keys.getPublicKey("hex")); +keys.setPublicKey("é", "latin1"); +console.log(keys.getPublicKey("hex")); +keys.setPrivateKey(new Uint16Array([0x0102, 0x0304])); +console.log(keys.getPrivateKey("hex")); +keys.setPrivateKey(new DataView(new Uint8Array([9, 8, 7]).buffer)); +console.log(keys.getPrivateKey("hex")); diff --git a/tests/corpus/6711-dh-handles.ts b/tests/corpus/6711-dh-handles.ts new file mode 100644 index 0000000000..c0c5522bea --- /dev/null +++ b/tests/corpus/6711-dh-handles.ts @@ -0,0 +1,56 @@ +// DiffieHellman and DiffieHellmanGroup values as ordinary values: class +// fields, optional fields, arrays, Map values, parameters and return +// values, with verifyError read through each. +import * as crypto from "node:crypto"; +import { type DiffieHellman, type DiffieHellmanGroup } from "node:crypto"; + +const prime = crypto.getDiffieHellman("modp1").getPrime(); + +class Party { + readonly dh: DiffieHellman; + peer: DiffieHellman | null = null; + group?: DiffieHellmanGroup; + + constructor(seed: number) { + this.dh = crypto.createDiffieHellman(prime, 2); + this.dh.setPrivateKey(Buffer.from([seed, seed + 1, seed + 2])); + this.dh.generateKeys(); + } + + secretWith(other: Party): string { + return this.dh.computeSecret(other.dh.getPublicKey(), null, "hex"); + } +} + +const alice = new Party(1); +const bob = new Party(7); +alice.peer = bob.dh; +console.log(alice.secretWith(bob) === bob.secretWith(alice), alice.secretWith(bob).slice(0, 32)); +console.log(alice.peer !== null && alice.peer.getPublicKey().equals(bob.dh.getPublicKey())); +console.log(alice.group === undefined); +alice.group = crypto.getDiffieHellman("modp2"); +console.log(alice.group.verifyError, alice.group.getPrime().length); + +function verify(dh: DiffieHellman | DiffieHellmanGroup): number { + return dh.verifyError; +} + +function make(bytes: number[]): DiffieHellman { + return crypto.createDiffieHellman(Buffer.from(bytes)); +} + +const all: (DiffieHellman | DiffieHellmanGroup)[] = [alice.dh, make([23]), make([24]), crypto.getDiffieHellman("modp5")]; +console.log(all.map(verify).join(",")); +for (const dh of all) console.log(dh.getGenerator("hex"), dh.getPrime().length); + +const byName = new Map(); +for (const name of ["modp1", "modp2"]) byName.set(name, crypto.createDiffieHellmanGroup(name)); +for (const [name, group] of byName) { + group.generateKeys(); + console.log(name, group.getGenerator("hex"), group.computeSecret(alice.dh.getPublicKey()).length); +} + +let maybe: DiffieHellman | undefined; +console.log(maybe?.verifyError); +maybe = make([0x0d]); +console.log(maybe?.verifyError, maybe.getPrime("hex")); diff --git a/tests/fixtures/node-types/dh.ts b/tests/fixtures/node-types/dh.ts new file mode 100644 index 0000000000..0b20b10562 --- /dev/null +++ b/tests/fixtures/node-types/dh.ts @@ -0,0 +1,22 @@ +import { + type DiffieHellman, + type DiffieHellmanGroup, + createDiffieHellman, + createDiffieHellmanGroup, + getDiffieHellman, +} from "node:crypto"; + +const group: DiffieHellmanGroup = getDiffieHellman("modp2"); +const alice: DiffieHellman = createDiffieHellman(group.getPrime(), group.getGenerator()); +const bob: DiffieHellman = createDiffieHellman(group.getPrime("hex"), "hex", 2); +alice.setPrivateKey(Buffer.from("0a0b0c", "hex")); +bob.setPrivateKey("0d0e0f", "hex"); +const alicePublic: Buffer = alice.generateKeys(); +const bobPublic: string = bob.generateKeys("base64"); +const fromAlice: string = alice.computeSecret(bobPublic, "base64", "hex"); +const fromBob: Buffer = bob.computeSecret(alicePublic); +console.log(fromAlice === fromBob.toString("hex"), fromAlice.slice(0, 16), alice.verifyError); +const named: DiffieHellmanGroup = createDiffieHellmanGroup("modp5"); +console.log(named.getPrime().length, named.getGenerator("hex"), bob.getPrivateKey("hex")); +const generated: string = named.generateKeys("hex"); +console.log(generated.length <= 384, named.getPublicKey("hex") === generated); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 16a74081fa..c6a8e90b7c 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -103,6 +103,12 @@ cipherStream.on("data", () => {}); import { createECDH } from "node:crypto"; createECDH("sect163k1"); createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); +/* DiffieHellman's Object.prototype members, and crypto.diffieHellman over + * asymmetric KeyObjects. */ +import { createDiffieHellman, diffieHellman, type KeyObject } from "node:crypto"; +createDiffieHellman(Buffer.from([23])).toString(); +declare const dhKey: KeyObject; +diffieHellman({ privateKey: dhKey, publicKey: dhKey }); /* WebCrypto beyond the HMAC slice: @types/node declares the whole * SubtleCrypto surface, and the methods outside importKey, sign, verify * and digest fence by name. */ diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 0bd13963d4..6ac45473f3 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -228,24 +228,42 @@ fenced.ts:105:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer 104 | createECDH("sect163k1"); 105 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 106 | /* WebCrypto beyond the HMAC slice: @types/node declares the whole + 106 | /* DiffieHellman's Object.prototype members, and crypto.diffieHellman over hint: Node reads the second argument as the key's input encoding and answers a Buffer; pass the output encoding third: computeSecret(key, undefined, encoding) -fenced.ts:110:9 - error SC2020: 'crypto.subtle.encrypt' is part of the standard library types but has no scriptc lowering yet +fenced.ts:109:1 - error SC2020: 'DiffieHellman.toString' is part of the standard library types but has no scriptc lowering yet - 109 | async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - 110 | await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); + 108 | import { createDiffieHellman, diffieHellman, type KeyObject } from "node:crypto"; + 109 | createDiffieHellman(Buffer.from([23])).toString(); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 110 | declare const dhKey: KeyObject; + + hint: generateKeys(), computeSecret(), getPrime(), getGenerator(), getPublicKey(), getPrivateKey(), setPublicKey(), setPrivateKey() and verifyError are supported + +fenced.ts:111:1 - error SC2020: 'crypto.diffieHellman' is typed by @types/node but has no scriptc lowering yet + + 110 | declare const dhKey: KeyObject; + 111 | diffieHellman({ privateKey: dhKey, publicKey: dhKey }); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 112 | /* WebCrypto beyond the HMAC slice: @types/node declares the whole + + hint: crypto.diffieHellman takes asymmetric KeyObjects, which the static runtime does not have; the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets + +fenced.ts:116:9 - error SC2020: 'crypto.subtle.encrypt' is part of the standard library types but has no scriptc lowering yet + + 115 | async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + 116 | await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); | ^~~~~~~~~~~~~~~~~~~~~ - 111 | } + 117 | } hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle -fenced.ts:115:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet +fenced.ts:121:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet - 114 | * compile; the other members fence by name. */ - 115 | const cores = navigator.hardwareConcurrency; + 120 | * compile; the other members fence by name. */ + 121 | const cores = navigator.hardwareConcurrency; | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 116 | void cores; + 122 | void cores; hint: of Node's navigator global only typeof navigator and navigator.userAgent compile \ No newline at end of file diff --git a/tests/harness/corpus-costs/6706-dh-groups.ts.json b/tests/harness/corpus-costs/6706-dh-groups.ts.json new file mode 100644 index 0000000000..b051df2742 --- /dev/null +++ b/tests/harness/corpus-costs/6706-dh-groups.ts.json @@ -0,0 +1 @@ +{ "plain": 4855, "san": 8228 } diff --git a/tests/harness/corpus-costs/6707-dh-explicit-params.ts.json b/tests/harness/corpus-costs/6707-dh-explicit-params.ts.json new file mode 100644 index 0000000000..95ab0f1b21 --- /dev/null +++ b/tests/harness/corpus-costs/6707-dh-explicit-params.ts.json @@ -0,0 +1 @@ +{ "plain": 6328, "san": 10420 } diff --git a/tests/harness/corpus-costs/6708-dh-errors.js.json b/tests/harness/corpus-costs/6708-dh-errors.js.json new file mode 100644 index 0000000000..743216cb98 --- /dev/null +++ b/tests/harness/corpus-costs/6708-dh-errors.js.json @@ -0,0 +1 @@ +{ "plain": 8044, "san": 10499 } diff --git a/tests/harness/corpus-costs/6709-dh-generate.ts.json b/tests/harness/corpus-costs/6709-dh-generate.ts.json new file mode 100644 index 0000000000..1e00ad64d1 --- /dev/null +++ b/tests/harness/corpus-costs/6709-dh-generate.ts.json @@ -0,0 +1 @@ +{ "plain": 6999, "san": 9388 } diff --git a/tests/harness/corpus-costs/6710-dh-encodings.cjs.json b/tests/harness/corpus-costs/6710-dh-encodings.cjs.json new file mode 100644 index 0000000000..15e5385e07 --- /dev/null +++ b/tests/harness/corpus-costs/6710-dh-encodings.cjs.json @@ -0,0 +1 @@ +{ "plain": 6048, "san": 8117 } diff --git a/tests/harness/corpus-costs/6711-dh-handles.ts.json b/tests/harness/corpus-costs/6711-dh-handles.ts.json new file mode 100644 index 0000000000..ecf09ecb19 --- /dev/null +++ b/tests/harness/corpus-costs/6711-dh-handles.ts.json @@ -0,0 +1 @@ +{ "plain": 5664, "san": 7161 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 8af6998ca6..87ca7b59ff 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -918,6 +918,53 @@ for (const run of [() => ecdh.getPublicKey.name, () => { ecdh.custom = 1; }]) { }); }); +// DiffieHellman primes past mbedTLS's 8192-bit limit throw a coded SC2020 +// error, as do property reads other than verifyError, property writes, and +// Object.prototype method calls on an untyped DiffieHellman. +describe("DiffieHellman runtime refusals", () => { + test("primes longer than 8192 bits throw SC2020", async () => { + const r = await compileAndRun( + "dh-long-prime", + `import { createDiffieHellman } from "node:crypto"; +const prime = Buffer.alloc(1025, 0xff); +for (const run of [() => createDiffieHellman(prime), () => createDiffieHellman(8193)]) { + try { + run(); + } catch (error) { + const e = error as Error & { code?: string }; + console.log(e.name, e.code, e.message); + } +} +console.log(createDiffieHellman(Buffer.alloc(1024, 0xff)).verifyError); +`, + ); + expect(r.exitCode).toBe(0); + const message = "DiffieHellman primes longer than 8192 bits have no native implementation"; + expect(r.stdout).toBe(`Error SC2020 ${message}\nError SC2020 ${message}\n1\n`); + expect(r.stderr).toBe(""); + }); + + test("DiffieHellman property reads, writes and Object.prototype calls throw SC2020", async () => { + const r = await compileAndRun( + "dh-properties", + `const crypto = require("node:crypto"); +const dh = crypto.getDiffieHellman("modp1"); +console.log(dh.verifyError); +for (const run of [() => dh.getPrime.name, () => { dh.verifyError = 1; }, () => dh.hasOwnProperty("verifyError")]) { + try { + run(); + } catch (e) { + console.log(e.name, e.code); + } +} +`, + "cjs", + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe("0\nError SC2020\nError SC2020\nError SC2020\n"); + }); +}); + // Node formats an unknown output encoding with %s, which prints a // function's source text and calls a toString the encoding's class // defines. The cipher runtime has neither, so both throw a coded SC2020 diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index dabb9b0bfc..2f336a1cd2 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -1114,6 +1114,20 @@ test("node-types: ECDH matches Node under @types/node", async () => { expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: DiffieHellman matches Node under @types/node", async () => { + const outDir = outDirFor("node-dh"); + const entry = join(nodeTypesDir, "dh.ts"); + const result = await compile(entry, { outPath: join(outDir, "dh"), outDir, sanitize }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: WebCrypto HMAC matches Node under @types/node", async () => { const outDir = outDirFor("node-webcrypto-hmac"); const entry = join(nodeTypesDir, "webcrypto-hmac.ts"); diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 6d028d597c..cbab9f6c91 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -48,6 +48,7 @@ const features: NativeLinkFeatures = { tlsCa: false, cipher: false, ecdh: false, + dh: false, webcrypto: false, perfLoop: false, }; diff --git a/tests/harness/self-hosting-runtime-pack.test.ts b/tests/harness/self-hosting-runtime-pack.test.ts index f4fde795c0..f170f96d18 100644 --- a/tests/harness/self-hosting-runtime-pack.test.ts +++ b/tests/harness/self-hosting-runtime-pack.test.ts @@ -49,6 +49,7 @@ const base: NativeLinkFeatures = { tlsCa: false, cipher: false, ecdh: false, + dh: false, webcrypto: false, perfLoop: false, }; From ca446b2c30b3ecf5347d64e241a766af9989d52c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 21:56:08 -0500 Subject: [PATCH 040/395] Read the tls.createServer SNICallback in source order The runtime call reads the cert and key options before the SNICallback, so a cert or key initializer after the callback that reassigned the callback's binding changed the callback the server used, and the server could serve the wrong certificate. When SNICallback comes before a cert or key entry, the callback is now saved just before the next of them is evaluated. With the callback read in order, a callback expression with side effects lowers instead of being refused. --- .../src/frontend/lowering/lower-server.ts | 27 +++++++++------ tests/corpus/6933-tls-sni-callback.ts | 34 ++++++++++++++++++- tests/diagnostics/tls-sni-callback-fences.ts | 2 -- .../tls-sni-callback-fences.ts.txt | 17 +++------- 4 files changed, 54 insertions(+), 26 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index c27b2513e6..058d7874aa 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -4762,6 +4762,7 @@ function lowerTlsServerOptions( let cert: IrExpr | null = null; let key: IrExpr | null = null; let sni: IrExpr | undefined; + let pendingSni: IrStmt | null = null; for (const prop of node.properties.nodes) { let initializer: ts.Expression | null; if ( @@ -4780,20 +4781,22 @@ function lowerTlsServerOptions( } const k = (prop.name as ts.Identifier | ts.StringLiteral).text; if (k === "SNICallback" && allowSni) { - // The callback is a value read (a function or a binding), so it may - // be read after cert and key without changing what the program sees. - if (initializer !== null && !sideEffectFreeOptionValue(initializer)) { - lowerer.noLowering( - `a ${what} SNICallback expression with side effects`, - prop, - "pass a function or a variable that holds one", - ); - } const value = initializer !== null ? lowerer.lowerExpr(initializer) : lowerer.lowerShorthandValue(prop as ts.ShorthandPropertyAssignment); - sni = checkSniCallbackValue(lowerer, value, prop, what); + const checked = checkSniCallbackValue(lowerer, value, prop, what); + if (cert !== null && key !== null) { + sni = checked; + } else { + // The runtime call reads cert and key first. A cert or key entry + // after the callback can change the binding the callback reads, + // so the callback is saved ahead of the next of them, in source + // order. + const saved = lowerer.declareHiddenLocal("%sniCallback", checked.type); + pendingSni = { kind: "varDecl", localId: saved.id, init: checked, loc: locOf(prop) }; + sni = varRef(saved.id, checked.type, locOf(prop)); + } continue; } if (k !== "cert" && k !== "key") { @@ -4857,6 +4860,10 @@ function lowerTlsServerOptions( "cert/key are PEM strings or Buffers here", ); } + if (pendingSni !== null) { + v = { kind: "seqExpr", stmts: [pendingSni], result: v, type: v.type, loc }; + pendingSni = null; + } if (k === "cert") cert = v; else key = v; } diff --git a/tests/corpus/6933-tls-sni-callback.ts b/tests/corpus/6933-tls-sni-callback.ts index 7958882d54..8c94428060 100644 --- a/tests/corpus/6933-tls-sni-callback.ts +++ b/tests/corpus/6933-tls-sni-callback.ts @@ -33,6 +33,30 @@ const server = tls.createServer( }, ); +// The callback is read where it appears, effects included: a later cert +// initializer that reassigns its binding does not change the callback the +// server uses. +type Sni = (servername: string, cb: (err: Error | null, ctx?: tls.SecureContext) => void) => void; +let chosen: Sni = (servername, cb) => { + console.log("first callback", servername); + cb(null, altContext); +}; +const ordered = tls.createServer( + { + SNICallback: (console.log("read callback"), chosen), + cert: (console.log("read cert"), + (chosen = (servername, cb) => { + console.log("second callback", servername); + cb(null, undefined); + }), + cert), + key, + }, + (socket) => { + socket.end("ordered"); + }, +); + function attempt(port: number, servername: string, next: () => void): void { let done = false; const finish = (line: string): void => { @@ -58,7 +82,15 @@ server.listen(0, "127.0.0.1", () => { attempt(port, "localhost", () => attempt(port, "alt.localhost", () => attempt(port, "other.localhost", () => - attempt(port, "fail.localhost", () => server.close(() => console.log("closed"))), + attempt(port, "fail.localhost", () => + server.close(() => { + console.log("closed"); + ordered.listen(0, "127.0.0.1", () => { + const orderedPort = (ordered.address() as net.AddressInfo).port; + attempt(orderedPort, "alt.localhost", () => ordered.close(() => console.log("ordered closed"))); + }); + }), + ), ), ), ); diff --git a/tests/diagnostics/tls-sni-callback-fences.ts b/tests/diagnostics/tls-sni-callback-fences.ts index 0c745365cd..20b6ce2711 100644 --- a/tests/diagnostics/tls-sni-callback-fences.ts +++ b/tests/diagnostics/tls-sni-callback-fences.ts @@ -9,7 +9,5 @@ function makeSni(): (servername: string, cb: Answer) => void { return (_servername, cb) => cb(null, undefined); } -// The callback expression has side effects. -tls.createServer({ cert, key, SNICallback: makeSni() }, (socket) => socket.end()); // SNICallback with options the runtime options walk reads. tls.createServer({ cert, key, ALPNProtocols: ["h2"], SNICallback: makeSni }); diff --git a/tests/harness/__snapshots__/tls-sni-callback-fences.ts.txt b/tests/harness/__snapshots__/tls-sni-callback-fences.ts.txt index 920e655323..c45608bb92 100644 --- a/tests/harness/__snapshots__/tls-sni-callback-fences.ts.txt +++ b/tests/harness/__snapshots__/tls-sni-callback-fences.ts.txt @@ -1,17 +1,8 @@ -tls-sni-callback-fences.ts:13:31 - error SC2020: 'a tls.createServer SNICallback expression with side effects' is part of the standard library types but has no scriptc lowering yet +tls-sni-callback-fences.ts:13:54 - error SC2020: 'tls.createServer with SNICallback and options the runtime walk reads' is part of the standard library types but has no scriptc lowering yet - 12 | // The callback expression has side effects. - 13 | tls.createServer({ cert, key, SNICallback: makeSni() }, (socket) => socket.end()); - | ^~~~~~~~~~~~~~~~~~~~~~ - 14 | // SNICallback with options the runtime options walk reads. - - hint: pass a function or a variable that holds one - -tls-sni-callback-fences.ts:15:54 - error SC2020: 'tls.createServer with SNICallback and options the runtime walk reads' is part of the standard library types but has no scriptc lowering yet - - 14 | // SNICallback with options the runtime options walk reads. - 15 | tls.createServer({ cert, key, ALPNProtocols: ["h2"], SNICallback: makeSni }); + 12 | // SNICallback with options the runtime options walk reads. + 13 | tls.createServer({ cert, key, ALPNProtocols: ["h2"], SNICallback: makeSni }); | ^~~~~~~~~~~~~~~~~~~~ - 16 | + 14 | hint: SNICallback lowers with cert and key; ALPNProtocols, minVersion, maxVersion and the validated options are not combined with it yet \ No newline at end of file From 71d5e92dc044b4d719d8ae0f76d11751619fb499 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 22:02:22 -0500 Subject: [PATCH 041/395] Address review feedback on properties untyped code adds to typed objects --- packages/runtime/src/scr_json.c | 42 ++++++++++++------- .../corpus/6990-record-writes-from-js/main.ts | 6 +++ 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index bd9bc2cc29..54411bd54b 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2981,30 +2981,40 @@ static bool scr_jb_put_source_field(ScrJsonBuf *b, void *rec, const ScrRecordSou * a source, otherwise writes nothing and answers false. */ static bool scr_dyn_key_is_index(const char *key, size_t len, double *out); +typedef struct { + double index; + size_t entry; +} ScrRecordSourceIntegerKey; + +static int scr_record_source_integer_cmp(const void *a, const void *b) { + double x = ((const ScrRecordSourceIntegerKey *)a)->index; + double y = ((const ScrRecordSourceIntegerKey *)b)->index; + return (x > y) - (x < y); +} + /* The entry indexes of `view` in JavaScript's own-key order: integer keys * ascending, then the other keys in insertion order. `out` holds len. */ static void scr_record_source_key_order(const ScrDyn *view, size_t *out) { - size_t integers = 0; - for (size_t i = 0; i < view->v.obj.len; i++) { - double index; + size_t len = view->v.obj.len; + ScrRecordSourceIntegerKey small[64]; + ScrRecordSourceIntegerKey *keys = len <= 64 ? small : malloc(len * sizeof *keys); + if (!keys) scr_json_oom(); + size_t integers = 0, next = 0; + for (size_t i = 0; i < len; i++) { const ScrDynEntry *entry = &view->v.obj.entries[i]; - if (!scr_dyn_key_is_index(entry->key, entry->key_len, &index)) continue; - size_t at = integers++; - for (; at > 0; at--) { - double before; - const ScrDynEntry *previous = &view->v.obj.entries[out[at - 1]]; - scr_dyn_key_is_index(previous->key, previous->key_len, &before); - if (before < index) break; - out[at] = out[at - 1]; - } - out[at] = i; - } - size_t next = integers; - for (size_t i = 0; i < view->v.obj.len; i++) { double index; + if (scr_dyn_key_is_index(entry->key, entry->key_len, &index)) + keys[integers++] = (ScrRecordSourceIntegerKey){ index, i }; + } + /* Integer keys are distinct, so the order is total. */ + qsort(keys, integers, sizeof *keys, scr_record_source_integer_cmp); + for (size_t i = 0; i < integers; i++) out[next++] = keys[i].entry; + for (size_t i = 0; i < len; i++) { const ScrDynEntry *entry = &view->v.obj.entries[i]; + double index; if (!scr_dyn_key_is_index(entry->key, entry->key_len, &index)) out[next++] = i; } + if (keys != small) free(keys); } bool scr_jb_try_record_source(ScrJsonBuf *b, void *rec, diff --git a/tests/corpus/6990-record-writes-from-js/main.ts b/tests/corpus/6990-record-writes-from-js/main.ts index 2b343efc5f..2ca26b3e27 100644 --- a/tests/corpus/6990-record-writes-from-js/main.ts +++ b/tests/corpus/6990-record-writes-from-js/main.ts @@ -21,3 +21,9 @@ write(config, "debug", true); write(config, "tags", ["a", "b"]); console.log(JSON.stringify(config), keysOf(config)); console.log(config); + +// Many integer keys added in descending order serialize in ascending order. +const many = { name: "many" }; +for (let i = 3000; i > 0; i--) write(many, i, i); +const text = JSON.stringify(many); +console.log(text.length, text.slice(0, 40), text.slice(-30)); From d5688d7dc5efc2f1449aca0f16f634606f507443 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 22:02:39 -0500 Subject: [PATCH 042/395] Give shared helpers the location of their earliest use --- .../compiler/src/frontend/lowering/lowerer.ts | 100 ++++++++++++++---- .../coverage-explain-dynamic.txt | 2 +- 2 files changed, 79 insertions(+), 23 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 870e21b1f8..ee8ba7ecba 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -36,7 +36,8 @@ import { import { identityPreservingWidening } from "./coercions/identity.js"; import { scanDivergences } from "./divergences.js"; import { applyKeyOrderWitnesses, KeyOrderNotes } from "./key-order-witness.js"; -import { observeRecordLiteralRebuilds } from "../../ir/traverse.js"; +import { exprFunctionReference } from "../../ir/function-references.js"; +import { everyStmtList, observeRecordLiteralRebuilds } from "../../ir/traverse.js"; import { checkedClassAssertion } from "./class-assertions.js"; import { narrowStoredClassValue, narrowGenericClassValue } from "./class-unions.js"; import { @@ -3266,11 +3267,13 @@ export class Lowerer { if (typeof helper === "string") this.sharedHelperNames.add(helper); else this.sharedHelpers.add(helper); } - /** Gives each shared helper the location of the entry file's start: the - * helpers marked by shareHelper, and those interned by key in the helper - * tables. */ - private settleSharedHelperLocations(): void { - const loc: SrcLoc = { file: this.entry.fileName, start: 0, end: 0 }; + /** Gives each shared helper (those marked by shareHelper, and those + * interned by key in the helper tables) one location for all its nodes: + * the earliest site in `functions` that uses it, by file and position, so + * reports such as --explain-dynamic point at a use whatever order bodies + * lowered in. A helper used only by other shared helpers takes their + * location; one with no use takes the entry file's start. */ + private settleSharedHelperLocations(functions: readonly IrFunction[]): void { const interned = new Set([...this.sharedHelperNames, ...this.classMethodValueHelpers]); for (const table of [ this.equalityHelpers, @@ -3293,21 +3296,68 @@ export class Lowerer { for (const name of table.values()) interned.add(name); for (const fn of [...this.liftedFns, ...this.implicitFns]) if (interned.has(fn.name)) this.sharedHelpers.add(fn); - const seen = new Set(); - const visit = (node: unknown): void => { - if (typeof node !== "object" || node === null || seen.has(node)) return; - seen.add(node); - if (Array.isArray(node)) { - for (const item of node) visit(item); - return; - } - const record = node as Record; - for (const key of Object.keys(record)) { - if (key === "loc") record[key] = loc; - else if (key !== "type") visit(record[key]); + if (this.sharedHelpers.size === 0) return; + const shared = new Map([...this.sharedHelpers].map((fn) => [fn.name, fn])); + const earlier = (left: SrcLoc, right: SrcLoc): boolean => + left.file < right.file || (left.file === right.file && left.start < right.start); + // The earliest use of each shared helper outside the shared helpers, and + // which shared helpers each shared helper uses. + const uses = new Map(); + const usedBy = new Map>(); + for (const fn of functions) { + const inShared = shared.has(fn.name) ? fn.name : undefined; + everyStmtList(fn.body, { + stmt: () => true, + expr: (expr) => { + const name = expr.kind === "call" ? expr.callee : exprFunctionReference(expr); + if (name === undefined || name === inShared || !shared.has(name)) return true; + if (inShared !== undefined) { + let users = usedBy.get(name); + if (users === undefined) usedBy.set(name, (users = new Set())); + users.add(inShared); + } else { + const current = uses.get(name); + if (current === undefined || earlier(expr.loc, current)) uses.set(name, expr.loc); + } + return true; + }, + }); + } + // Helpers reached only through other shared helpers take the earliest + // location among those, to a fixed point. + for (let changed = true; changed;) { + changed = false; + for (const [name, users] of usedBy) { + for (const user of users) { + const loc = uses.get(user); + const current = uses.get(name); + if (loc !== undefined && (current === undefined || earlier(loc, current))) { + uses.set(name, loc); + changed = true; + } + } } - }; - for (const fn of this.sharedHelpers) visit(fn); + } + const fallback: SrcLoc = { file: this.entry.fileName, start: 0, end: 0 }; + for (const fn of this.sharedHelpers) { + const use = uses.get(fn.name) ?? fallback; + const loc: SrcLoc = { file: use.file, start: use.start, end: use.end }; + const seen = new Set(); + const visit = (node: unknown): void => { + if (typeof node !== "object" || node === null || seen.has(node)) return; + seen.add(node); + if (Array.isArray(node)) { + for (const item of node) visit(item); + return; + } + const record = node as Record; + for (const key of Object.keys(record)) { + if (key === "loc") record[key] = loc; + else if (key !== "type") visit(record[key]); + } + }; + visit(fn); + } } /** The first lifted function named `name`, the one * `liftedFns.find((fn) => fn.name === name)` returns. */ @@ -7401,7 +7451,7 @@ export class Lowerer { // Lambdas lifted while lowering any of the above (plus synthetic // array-HOF loop functions, which ride the same list), and the // implicit-any instances lowered eagerly at their first call sites. - this.settleSharedHelperLocations(); + this.settleSharedHelperLocations([...functions, ...this.liftedFns, ...this.implicitFns]); functions.push(...this.liftedFns); functions.push(...this.implicitFns); functions.push(...runtimeClassConstructs(this, functions)); @@ -8116,7 +8166,13 @@ export class Lowerer { ]); observeRecordLiteralRebuilds(null); for (const finalize of this.shapeOrderHelperFinalizers) finalize(); - this.settleSharedHelperLocations(); + this.settleSharedHelperLocations([ + ...orderedUnits, + ...initFunctions, + ...instanceFunctions, + ...this.liftedFns, + ...this.implicitFns, + ]); // Lifted and implicit functions are created in lowering order; they // are assembled by name. const functions = [ diff --git a/tests/harness/__snapshots__/coverage-explain-dynamic.txt b/tests/harness/__snapshots__/coverage-explain-dynamic.txt index f168988e6d..6e561634d2 100644 --- a/tests/harness/__snapshots__/coverage-explain-dynamic.txt +++ b/tests/harness/__snapshots__/coverage-explain-dynamic.txt @@ -1,8 +1,8 @@ dynamic tier (tier assignment, report-only; --tier=auto) 4 functions already operate on dynamic values (1 statement) readName tests/coverage-fixtures/tier-demotions.ts:15:1: %dyn.error.stringConstructor at tests/coverage-fixtures/tier-demotions.ts:17:10 - %dyn.error.stringConstructor.0 tests/coverage-fixtures/tier-demotions.ts:17:10: dynCall at tests/coverage-fixtures/tier-demotions.ts:17:10 %dyn.class.property.0 tests/coverage-fixtures/tier-demotions.ts:17:17: %dyn.class.readPropertiesOrEmpty at tests/coverage-fixtures/tier-demotions.ts:17:17 + %dyn.error.stringConstructor.0 tests/coverage-fixtures/tier-demotions.ts:17:10: dynCall at tests/coverage-fixtures/tier-demotions.ts:17:10 %method.value:%%Error.toString tests/coverage-fixtures/tier-demotions.ts:17:10: dyn.this at tests/coverage-fixtures/tier-demotions.ts:17:10 2 demotion requests (refusals and fences the dynamic tier would replace) From fc551c4a8bf8fad4294d7de85d1a045005309d39 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 20 Oct 2026 19:25:56 -0700 Subject: [PATCH 043/395] Key weak collections by net sockets and servers, and carry the socket in undici client diagnostics Co-Authored-By: Claude Opus 5.5 --- packages/compiler/shims/undici/core/client.ts | 8 +- packages/compiler/src/ir/ir.ts | 4 +- .../corpus/6620-weak-keys-net-sockets.ts.json | 6 ++ packages/runtime/src/scr_json.c | 3 +- packages/runtime/src/scr_net.c | 2 + tests/corpus/6620-weak-keys-net-sockets.ts | 81 +++++++++++++++++++ tests/fixtures/npm/cases/undici5/main.ts | 35 +++++++- tests/fixtures/npm/cases/undici7/main.ts | 35 +++++++- 8 files changed, 166 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6620-weak-keys-net-sockets.ts.json create mode 100644 tests/corpus/6620-weak-keys-net-sockets.ts diff --git a/packages/compiler/shims/undici/core/client.ts b/packages/compiler/shims/undici/core/client.ts index f391e6d274..ddc1ae22e7 100644 --- a/packages/compiler/shims/undici/core/client.ts +++ b/packages/compiler/shims/undici/core/client.ts @@ -370,7 +370,7 @@ export class Client extends DispatcherBase { localAddress: this.__localAddress, }; if (channels.beforeConnect.hasSubscribers) { - channels.beforeConnect.publish({ connectParams }); + channels.beforeConnect.publish({ connectParams, connector: this.__connector }); } let settled = false; const onConnected = (err: Error | null, socket: Socket | null): void => { @@ -428,7 +428,7 @@ export class Client extends DispatcherBase { if (this.__socket === socket) this.__onSocketClose(socket); }); if (channels.connected.hasSubscribers) { - channels.connected.publish({ connectParams }); + channels.connected.publish({ connectParams, connector: this.__connector, socket }); } this.emit("connect", this.__url, [this as Dispatcher]); this.__resume(false); @@ -447,7 +447,7 @@ export class Client extends DispatcherBase { ): void { this.__connecting = false; if (channels.connectError.hasSubscribers) { - channels.connectError.publish({ connectParams, error: err }); + channels.connectError.publish({ connectParams, connector: this.__connector, error: err }); } this.__onError(err); this.emit("connectionError", this.__url, [this as Dispatcher], err); @@ -658,7 +658,7 @@ export class Client extends DispatcherBase { } header += request.headers; if (channels.sendHeaders.hasSubscribers) { - channels.sendHeaders.publish({ request: request.diagnosticView(), headers: header }); + channels.sendHeaders.publish({ request: request.diagnosticView(), headers: header, socket }); } if (body === null) { if (contentLength === 0) socket.write(`${header}content-length: 0\r\n\r\n`); diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 3c89d4009a..6ffba9cd38 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -4331,7 +4331,9 @@ function canBoxDynCompositeAt( } } default: - return false; + // Runtime handles (a socket in a diagnostics message) box by + // reference wherever the walker meets them, as at the top level. + return DYN_HANDLE_KINDS.has(t.kind); } } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6620-weak-keys-net-sockets.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6620-weak-keys-net-sockets.ts.json new file mode 100644 index 0000000000..f5ef586061 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6620-weak-keys-net-sockets.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6620-weak-keys-net-sockets.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index d9c1e5fab2..e6b468699c 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -13769,7 +13769,8 @@ static int scr_weak_key(const ScrDyn *key, void **ptr, unsigned *kind) { 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_FETCH_REQUEST && key->v.handle.tag != SCR_DYNH_FETCH_RESPONSE && - key->v.handle.tag != SCR_DYNH_MAP && key->v.handle.tag != SCR_DYNH_SET) return -1; + 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; *ptr = key->v.handle.ptr; *kind = 4 + key->v.handle.tag; return 1; case SCR_DYN_TYPED_REF: if (!key->v.typed_ref.observed) return -1; diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 765b59a05a..8f51f7a608 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -678,6 +678,7 @@ void scr_net_server_release(ScrNetServer *s) { #ifdef SCR_RC_AUDIT scr_net_live--; #endif + scr_weak_dispose_inline(s); /* weak collections keyed by this socket or server */ free(s); } } @@ -724,6 +725,7 @@ void scr_net_sock_release(ScrNetSocket *s) { #ifdef SCR_RC_AUDIT scr_net_live--; #endif + scr_weak_dispose_inline(s); /* weak collections keyed by this socket or server */ free(s); } } diff --git a/tests/corpus/6620-weak-keys-net-sockets.ts b/tests/corpus/6620-weak-keys-net-sockets.ts new file mode 100644 index 0000000000..65878887e1 --- /dev/null +++ b/tests/corpus/6620-weak-keys-net-sockets.ts @@ -0,0 +1,81 @@ +// Sockets and servers key WeakMaps and WeakSets by identity: the socket a +// connect callback sees, the one a 'connection' listener sees, and the +// same socket read back out of a diagnostics_channel message are one key. +// Entries of closed sockets stop answering once the socket is gone. +import * as net from "node:net"; +import type { Server, Socket } from "node:net"; +import { channel } from "node:diagnostics_channel"; + +interface Conn { + label: string; + bytes: number; +} + +const connections = new WeakMap(); +const seen = new WeakSet(); +const servers = new WeakMap(); +const opened = channel("example:socket:opened"); + +opened.subscribe((message) => { + const { socket, label } = message as { socket: Socket; label: string }; + console.log(`[diag] ${label} known ${connections.has(socket)}, seen ${seen.has(socket)}`); + const conn = connections.get(socket); + if (conn !== undefined) conn.label = `${conn.label}+diag`; +}); + +// Each dial waits for both ends to close, so the two sides log in order. +let serverClosed: () => void = () => {}; + +const server = net.createServer((socket) => { + seen.add(socket); + connections.set(socket, { label: "server side", bytes: 0 }); + socket.on("data", (chunk) => { + const conn = connections.get(socket); + if (conn !== undefined) conn.bytes += chunk.length; + socket.end(`echo ${chunk.toString()}`); + }); + socket.on("close", () => { + const conn = connections.get(socket); + console.log("server socket closed", conn?.label, conn?.bytes); + console.log("delete", connections.delete(socket), connections.delete(socket)); + serverClosed(); + }); +}); +servers.set(server, "echo server"); + +function dial(port: number, label: string): Promise { + const bothClosed = new Promise((resolve) => { + serverClosed = resolve; + }); + const clientClosed = new Promise((resolve) => { + const client: Socket = net.connect(port, "127.0.0.1", () => { + connections.set(client, { label, bytes: 0 }); + console.log(label, "connected; has", connections.has(client), "seen", seen.has(client)); + opened.publish({ socket: client, label }); + client.write(label); + }); + let reply = ""; + client.on("data", (chunk) => { + reply += chunk.toString(); + }); + client.on("close", () => { + console.log(label, "reply", JSON.stringify(reply), "entry", connections.get(client)?.label); + resolve(); + }); + }); + return Promise.all([clientClosed, bothClosed]).then(() => undefined); +} + +server.listen(0, "127.0.0.1", async () => { + console.log("server known", servers.has(server), servers.get(server)); + const address = server.address(); + const port = typeof address === "object" && address !== null ? address.port : 0; + await dial(port, "first"); + await dial(port, "second"); + try { + connections.set(1 as unknown as Socket, { label: "number", bytes: 0 }); + } catch (err) { + console.log((err as Error).name, (err as Error).message); + } + server.close(() => console.log("closed; server still known", servers.has(server))); +}); diff --git a/tests/fixtures/npm/cases/undici5/main.ts b/tests/fixtures/npm/cases/undici5/main.ts index b51b16c65b..ae39bacf9c 100644 --- a/tests/fixtures/npm/cases/undici5/main.ts +++ b/tests/fixtures/npm/cases/undici5/main.ts @@ -81,6 +81,34 @@ channel("undici:request:trailers").subscribe((message) => { if (t !== undefined) console.log(` [diag] ${t.label} done after ${t.dispatched} dispatch`); }); +// A transport's connection registry: sockets the wrapped connector dialed, +// keyed weakly, and the client diagnostics messages that carry them. +const dialed = new WeakMap(); +const clientDiag = { beforeConnect: 0, connector: 0, connected: 0, sendHeaders: 0, known: 0, unknown: 0 }; +const noteSocket = (socket: Socket): void => { + const n = dialed.get(socket); + if (n === undefined) clientDiag.unknown++; + else clientDiag.known++; +}; +channel("undici:client:beforeConnect").subscribe((message) => { + const { connector } = message as { connector: unknown }; + clientDiag.beforeConnect++; + if (typeof connector === "function") clientDiag.connector++; +}); +channel("undici:client:connected").subscribe((message) => { + const { socket, connectParams } = message as { + socket: Socket; + connectParams: { protocol: string; hostname: string }; + }; + clientDiag.connected++; + if (connectParams.protocol === "http:" && connectParams.hostname === "127.0.0.1") noteSocket(socket); +}); +channel("undici:client:sendHeaders").subscribe((message) => { + const { socket, headers } = message as { socket: Socket; headers: string }; + clientDiag.sendHeaders++; + if (headers.length > 0) noteSocket(socket); +}); + /** A transport-style Agent: one Pool per origin built by the factory, a * wrapped connector, and a Client interceptor that marks dispatches. */ class TracingAgent extends Agent { @@ -102,7 +130,9 @@ class TracingAgent extends Agent { pipelining: 1, connect(options, callback) { opened++; - return connect(options, callback); + const socket = connect(options, callback); + dialed.set(socket, opened); + return socket; }, interceptors: { Client: [ @@ -198,6 +228,9 @@ server.listen(0, "127.0.0.1", async () => { await r.body.text(); } console.log("connections opened:", agent.connections); + console.log( + `client diagnostics: ${clientDiag.beforeConnect} before connect (${clientDiag.connector} with a connector), ${clientDiag.connected} connected, ${clientDiag.sendHeaders} send headers; ${clientDiag.known} carry a dialed socket, ${clientDiag.unknown} another`, + ); console.log("stats:", agent.statsFor(origin)); console.log("== Pool with a connection limit"); diff --git a/tests/fixtures/npm/cases/undici7/main.ts b/tests/fixtures/npm/cases/undici7/main.ts index f39e3031d2..28cfd83530 100644 --- a/tests/fixtures/npm/cases/undici7/main.ts +++ b/tests/fixtures/npm/cases/undici7/main.ts @@ -115,6 +115,34 @@ channel("undici:request:trailers").subscribe((message) => { if (t !== undefined) console.log(` [diag] ${t.label} done after ${t.dispatched} dispatch`); }); +// A transport's connection registry: sockets the wrapped connector dialed, +// keyed weakly, and the client diagnostics messages that carry them. +const dialed = new WeakMap(); +const clientDiag = { beforeConnect: 0, connector: 0, connected: 0, sendHeaders: 0, known: 0, unknown: 0 }; +const noteSocket = (socket: Socket): void => { + const n = dialed.get(socket); + if (n === undefined) clientDiag.unknown++; + else clientDiag.known++; +}; +channel("undici:client:beforeConnect").subscribe((message) => { + const { connector } = message as { connector: unknown }; + clientDiag.beforeConnect++; + if (typeof connector === "function") clientDiag.connector++; +}); +channel("undici:client:connected").subscribe((message) => { + const { socket, connectParams } = message as { + socket: Socket; + connectParams: { protocol: string; hostname: string }; + }; + clientDiag.connected++; + if (connectParams.protocol === "http:" && connectParams.hostname === "127.0.0.1") noteSocket(socket); +}); +channel("undici:client:sendHeaders").subscribe((message) => { + const { socket, headers } = message as { socket: Socket; headers: string }; + clientDiag.sendHeaders++; + if (headers.length > 0) noteSocket(socket); +}); + /** A transport-style Agent: one Pool per origin built by the factory, a * wrapped connector, and a Client interceptor that marks dispatches. */ class TracingAgent extends Agent { @@ -136,7 +164,9 @@ class TracingAgent extends Agent { pipelining: 1, connect(options, callback) { opened++; - return connect(options, callback); + const socket = connect(options, callback); + dialed.set(socket, opened); + return socket; }, interceptors: { Client: [ @@ -232,6 +262,9 @@ server.listen(0, "127.0.0.1", async () => { await r.body.text(); } console.log("connections opened:", agent.connections); + console.log( + `client diagnostics: ${clientDiag.beforeConnect} before connect (${clientDiag.connector} with a connector), ${clientDiag.connected} connected, ${clientDiag.sendHeaders} send headers; ${clientDiag.known} carry a dialed socket, ${clientDiag.unknown} another`, + ); console.log("stats:", agent.statsFor(origin)); console.log("== Pool with a connection limit"); From 4100cd3c047601e5a13f593f99a40894be3d8611 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 22:08:30 -0500 Subject: [PATCH 044/395] Never buffer instances of classes whose trace visits nothing A class carries the collector header when any class in its hierarchy can be on a cycle, so instances of classes whose own fields hold nothing cycle-capable were buffered as candidates and walked by every level. Such an object has no traced children and is never on a cycle. Their descriptors now name scr_cyc_no_children, and they are allocated with buf set to SCR_CYC_ACYCLIC. That value has the buffered bit set, so a release that leaves the object alive makes no call; scr_cyc_is_buffered is false for it, and an emitted death or publication mark takes the object out of a candidate buffer only when buf is below -1. A pass that walks such an object as a child restores the value when it survives. --- packages/compiler/src/backend/llvm/alloc.ts | 9 +- .../compiler/src/backend/llvm/classes.test.ts | 38 ++++++ packages/compiler/src/backend/llvm/classes.ts | 15 ++- .../compiler/src/backend/llvm/publish.test.ts | 3 + packages/compiler/src/backend/llvm/publish.ts | 5 +- packages/compiler/src/backend/llvm/shapes.ts | 15 +-- ...clic-classes-in-traced-hierarchies.ts.json | 6 + packages/runtime/src/scr_cycle.c | 30 ++++- packages/runtime/src/scr_rc.h | 32 ++++- packages/runtime/test/test_cycle.c | 33 +++++ .../acyclic-classes-in-traced-hierarchies.ts | 117 ++++++++++++++++++ ...clic-classes-in-traced-hierarchies.ts.json | 1 + 12 files changed, 279 insertions(+), 25 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/acyclic-classes-in-traced-hierarchies.ts.json create mode 100644 tests/corpus/acyclic-classes-in-traced-hierarchies.ts create mode 100644 tests/harness/corpus-costs/acyclic-classes-in-traced-hierarchies.ts.json diff --git a/packages/compiler/src/backend/llvm/alloc.ts b/packages/compiler/src/backend/llvm/alloc.ts index c28a63e622..aa6345dabf 100644 --- a/packages/compiler/src/backend/llvm/alloc.ts +++ b/packages/compiler/src/backend/llvm/alloc.ts @@ -244,9 +244,12 @@ export function cycleAppendHelper(): string[] { } /** A collector header to install: the object type's constant ScrCycType - * descriptor (its trace and teardown; see cycTypeDescriptor). */ + * descriptor (its trace and teardown; see cycTypeDescriptor). `acyclic`: + * the descriptor's trace is scr_cyc_no_children, and `buf` starts out + * SCR_CYC_ACYCLIC (scr_cyc_alloc does the same on its path). */ export interface CycHeader { type: string; + acyclic?: boolean; } /** The first collector statistics index of an emitted ScrCycType @@ -391,7 +394,9 @@ function allocLines( ` %sa.cgp = getelementptr inbounds i8, ptr %sa.blk, i64 2`, ` store i16 0, ptr %sa.cgp ; ScrCycHdr.color, gen`, ` %sa.bufp = getelementptr inbounds i8, ptr %sa.blk, i64 ${CYC_BUF}`, - ` store i32 0, ptr %sa.bufp ; ScrCycHdr.buf`, + cyc.acyclic === true + ? ` store i32 -1, ptr %sa.bufp ; ScrCycHdr.buf = SCR_CYC_ACYCLIC` + : ` store i32 0, ptr %sa.bufp ; ScrCycHdr.buf`, ` %sa.live = load i64, ptr @scr_cyc_live`, ` %sa.live1 = add i64 %sa.live, 1`, ` store i64 %sa.live1, ptr @scr_cyc_live`, diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index 5daaf43002..94da472d45 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -157,6 +157,44 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts", expect(body(mangleClassRelease("Owner"))).toContain("load i32, ptr %bufp"); }); +test("classes whose trace visits nothing start acyclic in a traced hierarchy", () => { + const mapper = { kind: "object" as const, className: "Mapper" }; + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }], + classes: [ + { name: "Mapper", fields: [], methods: [], loc }, + // A dyn field can reach anything: the hierarchy carries collector headers. + { + name: "Linked", + base: "Mapper", + fields: [ + { name: "next", type: mapper }, + { name: "hook", type: DYN }, + ], + methods: [], + loc, + }, + { name: "Simple", base: "Mapper", fields: [{ name: "value", type: F64 }], methods: [], loc }, + ], + }; + const llvm = emitLlvmModule(mod, { pointerBits: 64 }); + const body = (name: string): string => + new RegExp(`^define internal [a-z]+ @${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(llvm)?.[0] ?? ""; + const descriptor = (cls: string): string => + llvm.split("\n").find((line) => line.startsWith(`@${mangleClassTrace(cls)}.cyctype = `)) ?? ""; + expect(descriptor("Simple")).toContain("{ ptr @scr_cyc_no_children, "); + expect(body(mangleClassNew("Simple"))).toContain("store i32 -1, ptr %sa.bufp"); + expect(descriptor("Linked")).toContain(`{ ptr @${mangleClassTrace("Linked")}, `); + expect(body(mangleClassNew("Linked"))).toContain("store i32 0, ptr %sa.bufp"); + // Releases share the hierarchy's possible-root step, which skips the + // call for SCR_CYC_ACYCLIC, and a death unbuffers only below -1. + expect(body(mangleClassRelease("Simple"))).toContain("icmp slt i32 %buf, 0"); + expect(body(mangleClassReleaseDirect("Simple"))).toContain("icmp slt i32 %dbuf, -1"); +}); + test("ProgramLifetime classes pack their fields after a 32-bit header", () => { const node = { kind: "object" as const, className: "Node" }; const leaf = { kind: "object" as const, className: "Leaf" }; diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 03ee8caa1c..7ca2216d2e 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -906,6 +906,15 @@ export function emitClassShapes( })), ]; const refFields = indexedFields.filter((f) => isRefCounted(f.type)); + const tracedFields = traced + ? indexedFields.filter((f) => traceAdapter(host, f.type) !== null) + : []; + // A traced class whose trace visits nothing carries the collector header + // only because its hierarchy is traced: it is never on a cycle. Its + // descriptor names scr_cyc_no_children and its objects start out + // SCR_CYC_ACYCLIC, so releases never buffer them. + const acyclic = + traced && runtimePrefix === null && !isStreamRooted && tracedFields.length === 0; const bounded = runtimePrefix !== null || isStreamRooted || @@ -1089,11 +1098,12 @@ export function emitClassShapes( `entry:`, ]; const cycType = `@${mangleClassTrace(cls.name)}.cyctype`; + if (acyclic) host.declare(`declare void @scr_cyc_no_children(ptr, ptr, ptr)`); if (traced) defs.push( cycTypeDescriptor( cycType, - `@${mangleClassTrace(cls.name)}`, + acyclic ? "@scr_cyc_no_children" : `@${mangleClassTrace(cls.name)}`, `@${mangleClassGcFree(cls.name)}`, host.nextCycKind?.() ?? 0, ), @@ -1113,7 +1123,7 @@ export function emitClassShapes( ...emitObjectAlloc( host, sizeOf, - traced && !lifetime ? { type: cycType } : null, + traced && !lifetime ? { type: cycType, acyclic } : null, storedHeaderBytes(meta), ), ); @@ -1188,7 +1198,6 @@ export function emitClassShapes( // exactly the complement, then free (the trace/teardown complement // contract in scr_runtime.h). const indexed = indexedFields; - const tracedFields = indexed.filter((f) => traceAdapter(host, f.type) !== null); const untracedRefFields = indexed.filter( (f) => isRefCounted(f.type) && traceAdapter(host, f.type) === null, ); diff --git a/packages/compiler/src/backend/llvm/publish.test.ts b/packages/compiler/src/backend/llvm/publish.test.ts index 5a84caa96f..c8008fdffa 100644 --- a/packages/compiler/src/backend/llvm/publish.test.ts +++ b/packages/compiler/src/backend/llvm/publish.test.ts @@ -60,6 +60,9 @@ test("publication walkers mark and push inline, with the runtime for the rest", expect(fn).toContain("icmp ult i64 %mrc, 4294967294"); expect(fn).toContain("getelementptr inbounds { ptr, i32, i32 }, ptr %mlog, i64 %mlogged"); expect(fn).toContain("store i64 -1, ptr %o"); + // A headered object leaves its candidate buffer only when `buf` is below + // -1: SCR_CYC_ACYCLIC (-1) marks an object that is never in one. + expect(fn).toContain("icmp sge i32 %mbufw, -1"); // Anything else goes through scr_pub_mark. expect(fn).toMatch(/call zeroext i1 @scr_pub_mark\(ptr %ctx, ptr %o, i1 (true|false), i32 4\)/); // A string is a leaf, marked at once; other references go on the stack, diff --git a/packages/compiler/src/backend/llvm/publish.ts b/packages/compiler/src/backend/llvm/publish.ts index ebe9b1a64a..bb7e0c8a86 100644 --- a/packages/compiler/src/backend/llvm/publish.ts +++ b/packages/compiler/src/backend/llvm/publish.ts @@ -434,12 +434,13 @@ export class PublishWalkers { let buf = classId || compact ? String(PUB_LOG_COUNT) : "0"; if (cyc) { // ScrCycHdr.buf, the word before the object: its top bit while the - // object is a buffered candidate. + // object is a buffered candidate, and -1 (SCR_CYC_ACYCLIC) for an + // object that never is one. this.host.declare(`declare i32 @scr_cyc_pub_take(ptr)`); lines.push( ` %mbufp = getelementptr inbounds i8, ptr %o, i64 -4`, ` %mbufw = load i32, ptr %mbufp`, - ` %mfree = icmp sge i32 %mbufw, 0`, + ` %mfree = icmp sge i32 %mbufw, -1`, ` br i1 %mfree, label %mput, label %mtake`, `mtake:`, ` %mix = call i32 @scr_cyc_pub_take(ptr %o)`, diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 58295e42be..6de92f6670 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -1369,7 +1369,7 @@ export function directChildReleaseLines( lines.push( ` %${tag}.bufp = getelementptr i8, ptr ${value}, ${S} -${host.cycleBufOffset}`, ` %${tag}.buf = load i32, ptr %${tag}.bufp`, - ` %${tag}.queued = icmp slt i32 %${tag}.buf, 0`, + ` %${tag}.queued = icmp slt i32 %${tag}.buf, -1 ; buffered, not SCR_CYC_ACYCLIC`, ` br i1 %${tag}.queued, label %${tag}.unbuffer, label %${tag}.teardown`, `${tag}.unbuffer:`, ` call void @scr_cyc_on_dead(ptr ${value})`, @@ -1595,8 +1595,9 @@ function requestCycleAppend(host: ShapeHost): boolean { /** The possible-cycle-root step of a release that left a headered object * alive: scr_cyc_on_release with its early return inlined. Test the sign - * of `buf` (the i32 at obj-4; negative for a queued candidate or a tenured - * object), and buffer a new candidate: through the module's + * of `buf` (the i32 at obj-4; negative for a queued candidate, a young + * object, or an object that is never a candidate, SCR_CYC_ACYCLIC), and + * buffer a new candidate: through the module's * cycleAppendHelper in optimized builds that support it, through * scr_cyc_on_release otherwise. Every emitted release of a headered object * shares this sequence (scr_runtime.h asserts the offsets); it ends by @@ -1619,16 +1620,16 @@ export function cycleRootLines(host: ShapeHost, done: string): string[] { } /** The death of a headered object: scr_cyc_on_dead with its early return - * inlined. Only an object still in a candidate buffer - * (`buffered` nonzero) has an entry to remove; the sequence continues at - * the new block `next`. */ + * inlined. Only an object still in a candidate buffer (`buf` negative and + * not SCR_CYC_ACYCLIC, which is -1) has an entry to remove; the sequence + * continues at the new block `next`. */ export function cycleDeadLines(host: ShapeHost, next: string): string[] { const S = host.sizeType; host.declare(`declare void @scr_cyc_on_dead(ptr)`); return [ ` %dbufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufOffset}`, ` %dbuf = load i32, ptr %dbufp`, - ` %dqueued = icmp slt i32 %dbuf, 0`, + ` %dqueued = icmp slt i32 %dbuf, -1 ; buffered, not SCR_CYC_ACYCLIC`, ` br i1 %dqueued, label %unbuffer, label %${next}`, `unbuffer:`, ` call void @scr_cyc_on_dead(ptr %o)`, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/acyclic-classes-in-traced-hierarchies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/acyclic-classes-in-traced-hierarchies.ts.json new file mode 100644 index 0000000000..8daa576cb1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/acyclic-classes-in-traced-hierarchies.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/acyclic-classes-in-traced-hierarchies.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index 591f544b81..5e5029e149 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -361,7 +361,15 @@ uint32_t scr_cyc_register(ScrCycType *type) { } void *scr_cyc_alloc(size_t size, ScrCycType *type) { - return scr_cyc_alloc_inline(size, type); + void *obj = scr_cyc_alloc_inline(size, type); + if (type->trace == scr_cyc_no_children) scr_cyc_hdr(obj)->buf = SCR_CYC_ACYCLIC; + return obj; +} + +void scr_cyc_no_children(void *obj, ScrTraceVisit visit, void *ctx) { + (void)obj; + (void)visit; + (void)ctx; } void scr_cyc_free(void *obj) { scr_cyc_free_inline(obj); } @@ -1175,7 +1183,13 @@ void scr_cyc_on_release(void *obj) { ScrCycHdr *h = scr_cyc_hdr(obj); /* Releasing an existing candidate cannot advance the trigger. A pass * always re-arms after its teardowns, including any candidates they add. */ - if (scr_cyc_is_buffered(h)) return; + if (scr_cyc_release_done(h)) return; + /* An object without cycle-capable children whose SCR_CYC_ACYCLIC a pass + * or the young-in nursery replaced gets it back. */ + if (scr_cyc_type(h)->trace == scr_cyc_no_children) { + h->buf = SCR_CYC_ACYCLIC; + return; + } #if SCR_CYC_INLINE_BUFFERING if (scr_cyc_append(h, obj)) return; #endif @@ -1346,14 +1360,17 @@ static void scr_scan_black(void *obj, uintptr_t depth) { ScrCycHdr *h = scr_cyc_hdr(obj); scr_cyc_set_color(h, SCR_CYC_BLACK); /* Drop markGray's component number: a survivor leaves the pass unbuffered - * unless it is a candidate that a limited pass did not take. */ - if (!scr_cyc_is_buffered(h)) scr_cyc_clear_buffered(h); + * unless it is a candidate that a limited pass did not take, and one + * without cycle-capable children leaves it SCR_CYC_ACYCLIC again. */ + const ScrCycType *type = scr_cyc_type(h); + bool acyclic = type->trace == scr_cyc_no_children; + if (!scr_cyc_is_buffered(h)) h->buf = acyclic ? SCR_CYC_ACYCLIC : 0; if (scr_cyc_gen(h) < SCR_CYC_OLD) scr_cyc_push_work(&scr_promote, obj); + if (acyclic) return; if (depth == SCR_CYC_WALK_DEPTH) { scr_cyc_push_work(&scr_restore_pending, obj); return; } - const ScrCycType *type = scr_cyc_type(h); SCR_CYC_CHILDREN(obj, type, scr_sb_array, scr_sb_visit, depth + 1); } static void scr_restore_from(void *root) { @@ -1881,7 +1898,8 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) { void *obj = scr_cands.v[i]; ScrCycHdr *h = scr_cyc_hdr(obj); if (scr_cyc_color(h) == SCR_CYC_DOOMED) continue; /* being freed below */ - if (!scr_cyc_is_buffered(h)) { + /* A candidate without cycle-capable children was never on a cycle. */ + if (!scr_cyc_is_buffered(h) && h->buf != SCR_CYC_ACYCLIC) { /* A survivor promoted into a level its type bypasses moves past * it, as a release would buffer it. */ if (scr_cyc_gen(h) == SCR_CYC_MATURE && scr_cyc_bypassed(scr_cyc_type(h), SCR_CYC_MATURE)) diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 9cb88e6e8b..892531965a 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -141,6 +141,11 @@ typedef struct ScrCycType { * Emitted allocation paths read it and take the slow path while it is 0. */ uint32_t id; } ScrCycType; +/* The trace of a type that holds nothing cycle-capable: it visits nothing. + * Emitted classes whose fields hold no cycle-capable value but whose + * hierarchy is traced name it (llvm/classes.ts); their objects are + * allocated with `buf` SCR_CYC_ACYCLIC. */ +void scr_cyc_no_children(void *obj, ScrTraceVisit visit, void *ctx); /* Runtime families' kinds. Emitted descriptors count from * SCR_CYC_KIND_EMITTED (llvm/alloc.ts FIRST_EMITTED_CYC_KIND). */ enum { @@ -169,7 +174,7 @@ enum { * collector color (SCR_CYC_*) and generation (SCR_CYC_NURSERY..SCR_CYC_OLD, * the walk filter), and `buf`: SCR_CYC_BUFFERED_BIT while the object sits in * its generation's candidate buffer, with its position there below it (O(1) - * removal when rc hits 0). */ + * removal when rc hits 0), or SCR_CYC_ACYCLIC. */ typedef struct ScrCycHdr { uint16_t id; uint8_t color; @@ -177,13 +182,25 @@ typedef struct ScrCycHdr { uint32_t buf; } ScrCycHdr; #define SCR_CYC_BUFFERED_BIT 0x80000000u +/* `buf` of an object whose type's trace is scr_cyc_no_children: it has no + * cycle-capable children, so it is never on a cycle and never a candidate. + * Such a type still carries the header when it shares a hierarchy with + * cycle-capable classes (headers are uniform per hierarchy). The value has + * the buffered bit set, so a release that leaves the object alive returns + * at the buffered test and makes no call; scr_cyc_is_buffered is false for + * it. No buffer position reaches it (scr_cyc_grow caps the buffers well + * below 2^31 entries). */ +#define SCR_CYC_ACYCLIC 0xffffffffu /* This layout is an ABI, not an implementation detail. The emitted - * allocation stores the id and zeroes the rest; every emitted release of a + * allocation stores the id and zeroes the rest (`buf` SCR_CYC_ACYCLIC for a + * class whose trace is scr_cyc_no_children); every emitted release of a * headered object (llvm/shapes.ts cycleRootLines, used by the record, class * and inline RC helpers) mirrors scr_cyc_on_release's early return by - * testing the sign of `buf` as an i32 at obj-4, and the emitted inline free - * reads the generation as an i8 at obj-5. Hence the assertions. */ + * testing the sign of `buf` as an i32 at obj-4, the emitted death mirrors + * scr_cyc_on_dead's by testing `buf` below -1 (cycleDeadLines), and the + * emitted inline free reads the generation as an i8 at obj-5. Hence the + * assertions. */ _Static_assert(sizeof(ScrCycHdr) == 8, "LLVM backend expects an 8-byte cycle header"); _Static_assert(offsetof(ScrCycHdr, id) == 0 && sizeof(((ScrCycHdr *)0)->id) == 2, "the emitted allocation stores the registry id as an i16 at the block start"); @@ -215,6 +232,11 @@ static inline void scr_cyc_set_color(ScrCycHdr *h, unsigned color) { h->color = static inline unsigned scr_cyc_gen(const ScrCycHdr *h) { return h->gen; } static inline void scr_cyc_set_gen(ScrCycHdr *h, unsigned gen) { h->gen = (uint8_t)gen; } static inline bool scr_cyc_is_buffered(const ScrCycHdr *h) { + return (h->buf & SCR_CYC_BUFFERED_BIT) != 0 && h->buf != SCR_CYC_ACYCLIC; +} +/* A release that leaves the object alive has nothing to buffer: it is a + * candidate already, or it is never one (SCR_CYC_ACYCLIC). */ +static inline bool scr_cyc_release_done(const ScrCycHdr *h) { return (h->buf & SCR_CYC_BUFFERED_BIT) != 0; } static inline uint32_t scr_cyc_buf_index(const ScrCycHdr *h) { @@ -608,7 +630,7 @@ static inline bool scr_cyc_append(ScrCycHdr *h, void *obj) { * Most releases of long-lived containers take the first early return. */ static inline void scr_cyc_released(void *obj) { ScrCycHdr *h = scr_cyc_hdr(obj); - if (scr_cyc_is_buffered(h)) return; + if (scr_cyc_release_done(h)) return; #if SCR_CYC_INLINE_BUFFERING if (scr_cyc_append(h, obj)) return; #endif diff --git a/packages/runtime/test/test_cycle.c b/packages/runtime/test/test_cycle.c index 723e9519fa..4e00b852e8 100644 --- a/packages/runtime/test/test_cycle.c +++ b/packages/runtime/test/test_cycle.c @@ -1434,6 +1434,38 @@ static void check_death_brings_full_pass(void) { free(keep); } +/* An object whose type's trace is scr_cyc_no_children starts out + * SCR_CYC_ACYCLIC and is never buffered. A pass that reaches it from a dead + * cycle frees it with the cycle when the cycle held its last reference, and + * otherwise leaves it SCR_CYC_ACYCLIC with its outside count. */ +static void check_acyclic_objects(void) { + static ScrCycType leaf_type = { &scr_cyc_no_children, &node_free, 0 }; + size_t before = freed; + Node *leaf = scr_cyc_alloc(sizeof(*leaf), &leaf_type); + ScrCycHdr *h = scr_cyc_hdr(leaf); + check(h->buf == SCR_CYC_ACYCLIC && !scr_cyc_is_buffered(h), "acyclic object did not start acyclic"); + leaf->rc = 2; /* an outside owner and a temporary reference */ + release_live(leaf); + check(h->buf == SCR_CYC_ACYCLIC, "release buffered an acyclic object"); + + Node *first = make_ring(2); + first->other = leaf; + leaf->rc++; + release_live(first); + scr_cyc_pass_at(SCR_CYC_NURSERY); + check(freed == before + 2, "nursery pass did not free the ring"); + check(leaf->rc == 1 && h->buf == SCR_CYC_ACYCLIC, "acyclic survivor lost its count or its mark"); + + Node *second = make_ring(2); + second->next->other = leaf; + leaf->rc++; + release_live(leaf); /* the outside owner lets go: only the ring holds it */ + check(h->buf == SCR_CYC_ACYCLIC, "release buffered an acyclic object"); + release_live(second); + scr_collect_cycles(); + check(freed == before + 5, "acyclic object held only by a dead ring was not freed with it"); +} + int main(void) { check_inline_buffering(); check_old_garbage_reclaimed_while_growing(); @@ -1467,6 +1499,7 @@ int main(void) { check_limited_pass(); check_large_old_buffer_with_nursery_candidates(); check_unjudged_backlog_follows_growth(); + check_acyclic_objects(); printf("cycle collection checks passed: threshold=%zu growth_cap=%zu\n", configured_nursery_threshold(), configured_growth_cap()); return 0; diff --git a/tests/corpus/acyclic-classes-in-traced-hierarchies.ts b/tests/corpus/acyclic-classes-in-traced-hierarchies.ts new file mode 100644 index 0000000000..d25def1dae --- /dev/null +++ b/tests/corpus/acyclic-classes-in-traced-hierarchies.ts @@ -0,0 +1,117 @@ +import { publish } from "@scriptc/threads"; + +// Classes whose own fields hold nothing cycle-capable, in a hierarchy where +// other classes form cycles: their instances are held by dead cycles, by +// live cycles and by plain locals while collections run, and are published +// with the contexts that hold them. + +abstract class Mapper { + abstract map(n: number): number; +} + +class Offset extends Mapper { + by: number; + constructor(by: number) { + super(); + this.by = by; + } + map(n: number): number { + return n + this.by; + } +} + +class Scale extends Offset { + readonly factor: number; + constructor(by: number, factor: number) { + super(by); + this.factor = factor; + } + map(n: number): number { + return super.map(n) * this.factor; + } +} + +class Context { + readonly steps: Mapper[] = []; + mapper: Mapper | undefined = undefined; + total = 0; +} + +class ContextMapper extends Mapper { + readonly context: Context; + constructor(context: Context) { + super(); + this.context = context; + } + map(n: number): number { + let value = n; + for (const step of this.context.steps) value = step.map(value); + return value; + } +} + +class Chain extends Mapper { + readonly first: Mapper; + next: Mapper | undefined = undefined; + constructor(first: Mapper) { + super(); + this.first = first; + } + map(n: number): number { + const value = this.first.map(n); + return this.next === undefined ? value : this.next.map(value); + } +} + +function makeContext(i: number, shared: Mapper): Context { + const context = new Context(); + context.steps.push(new Offset(i % 7), shared, new Scale(1, 2)); + context.mapper = new ContextMapper(context); + context.total = context.mapper.map(i); + return context; +} + +const shared = new Scale(3, 1); +const kept: Context[] = []; +let sum = 0; +for (let i = 0; i < 20000; i++) { + const context = makeContext(i, shared); + sum += context.total; + if (i % 1000 === 0) kept.push(context); +} + +// A chain that loops back to itself, holding mappers that are also held +// outside it. +const outside: Mapper[] = []; +let loops = 0; +for (let i = 0; i < 5000; i++) { + const offset = new Offset(i); + const head = new Chain(offset); + const tail = new Chain(new Scale(i, 3)); + head.next = tail; + tail.next = undefined; + loops += head.map(1); + tail.next = head; + if (i % 500 === 0) outside.push(offset); +} + +let keptTotal = 0; +for (const context of kept) keptTotal += context.mapper!.map(1); +let outsideTotal = 0; +for (const mapper of outside) outsideTotal += mapper.map(2); + +console.log(sum, loops, kept.length, keptTotal, outside.length, outsideTotal, shared.map(4)); + +publish(kept); +let publishedTotal = 0; +for (const context of kept) publishedTotal += context.mapper!.map(3); +console.log("published", publishedTotal); +try { + shared.by = 5; + console.log("write: ok"); +} catch (e) { + console.log(`write: ${(e as Error).name}: ${(e as Error).message}`); +} +const fresh = makeContext(7, new Offset(1)); +fresh.steps.push(shared); +console.log("after", fresh.mapper!.map(2), outside[3]!.map(1)); diff --git a/tests/harness/corpus-costs/acyclic-classes-in-traced-hierarchies.ts.json b/tests/harness/corpus-costs/acyclic-classes-in-traced-hierarchies.ts.json new file mode 100644 index 0000000000..bf904aec46 --- /dev/null +++ b/tests/harness/corpus-costs/acyclic-classes-in-traced-hierarchies.ts.json @@ -0,0 +1 @@ +{ "plain": 3350, "san": 3910 } From 2c5771e98cafcb2377a78ac03ca41d419ce1655b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 22:19:38 -0500 Subject: [PATCH 045/395] Run never-suspending async calls in place from stackless async bodies --- .../src/backend/llvm/emit-function.ts | 14 ++-- packages/compiler/src/backend/llvm/emitter.ts | 55 ++++++++++----- .../compiler/test/await-call-emission.test.ts | 68 +++++++++++++++++-- .../corpus/6953-fiberless-plain-calls.ts.json | 6 ++ tests/corpus/6953-fiberless-plain-calls.ts | 65 ++++++++++++++++++ .../6953-fiberless-plain-calls.ts.json | 1 + 6 files changed, 181 insertions(+), 28 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6953-fiberless-plain-calls.ts.json create mode 100644 tests/corpus/6953-fiberless-plain-calls.ts create mode 100644 tests/harness/corpus-costs/6953-fiberless-plain-calls.ts.json diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 0d5533f851..7bbd3d4538 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -1117,8 +1117,11 @@ export function emitAsyncAwaitCall( ``, ]; } - emitter.declare(`declare void @scr_async_inline_enter(ptr)`); - emitter.declare(`declare void @scr_async_inline_exit(ptr)`); + const framed = emitter.fiberlessFrames; + if (framed) { + emitter.declare(`declare void @scr_async_inline_enter(ptr)`); + emitter.declare(`declare void @scr_async_inline_exit(ptr)`); + } emitter.declare(`declare void @scr_await_hop()`); emitter.declare(`declare ptr @scr_promise_new()`); emitter.declare(`declare void @scr_promise_reject_pending(ptr)`); @@ -1127,11 +1130,12 @@ export function emitAsyncAwaitCall( const out = [ head, `entry:`, - ` %frame = alloca { ptr, ptr }`, - ` call void @scr_async_inline_enter(ptr %frame)`, + ...(framed + ? [` %frame = alloca { ptr, ptr }`, ` call void @scr_async_inline_enter(ptr %frame)`] + : []), retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`, ...emitter.pendingTestLines("%pend"), - ` call void @scr_async_inline_exit(ptr %frame)`, + ...(framed ? [` call void @scr_async_inline_exit(ptr %frame)`] : []), // Adopting: the then job's turn comes before the settled promise's // own reaction, whichever way the body completed. ...(adopt ? [` call void @scr_await_hop()`] : []), diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 4504ae58e6..0bdbe23b2b 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1798,16 +1798,33 @@ export class LlEmitter { }); } + /** Whether a fiberless body running on its caller's stack needs the + * frame scr_async_inline_enter/exit keep: it isolates the Error.stack + * frame chain, which only stack-observing programs push, and the + * AsyncLocalStorage context, which only programs that enter stores + * change (RuntimeFeatures asyncContext), as a fresh fiber would. Other + * programs run the body with a plain call. The frame also counts the + * bodies running on a fiber's stack for the runtime's trap on a fiberless + * body that suspends. */ + get fiberlessFrames(): boolean { + return this.stackTraces || this.schedulesInContext; + } + /** The spawn wrapper of a fiberless async function: runs the body on the - * caller's stack between scr_async_inline_enter/leave and settles the - * fresh promise from its outcome — the fiber trampoline's epilogue, - * without the fiber. */ + * caller's stack (between scr_async_inline_enter/leave when + * fiberlessFrames) and settles a fresh promise from its outcome — the + * fiber trampoline's epilogue, without the fiber. */ emitFiberlessAsyncSpawn(fn: IrFunction, spawnParams: string[]): string[] { const ret = fn.returnType; const retTy = this.llType(ret); const lifted = fn.captures !== undefined; - this.declare(`declare void @scr_async_inline_enter(ptr)`); - this.declare(`declare ptr @scr_async_inline_leave(ptr)`); + const framed = this.fiberlessFrames; + if (framed) { + this.declare(`declare void @scr_async_inline_enter(ptr)`); + this.declare(`declare ptr @scr_async_inline_leave(ptr)`); + } else { + this.declare(`declare ptr @scr_promise_new()`); + } this.declare(`declare void @scr_promise_reject_pending(ptr)`); if (lifted) { this.declare(`declare ptr @scr_closure_retain_v(ptr)`); @@ -1817,15 +1834,17 @@ export class LlEmitter { const out = [ `define internal ptr @${mangleAsyncSpawn(fn.name)}(${spawnParams.join(", ")}) ${FN_ATTRS} { ; fiberless ${fn.name}`, `entry:`, - ` %frame = alloca { ptr, ptr }`, + ...(framed ? [` %frame = alloca { ptr, ptr }`] : []), // The fiber path holds +1 on a lifted environment for the body's // duration (packed, released by the trampoline); keep that lifetime. ...(lifted ? [` %env = call ptr @scr_closure_retain_v(ptr %a0)`] : []), - ` call void @scr_async_inline_enter(ptr %frame)`, + ...(framed ? [` call void @scr_async_inline_enter(ptr %frame)`] : []), retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`, ...(lifted ? [` call void @scr_closure_release(ptr %a0)`] : []), ...this.pendingTestLines("%pend"), - ` %p = call ptr @scr_async_inline_leave(ptr %frame)`, + framed + ? ` %p = call ptr @scr_async_inline_leave(ptr %frame)` + : ` %p = call ptr @scr_promise_new()`, ` br i1 %pend, label %thrown, label %clean`, `clean:`, ...this.asyncFulfillLines(ret, "%p"), @@ -1864,23 +1883,27 @@ export class LlEmitter { } /** The inline await entry of a fiberless async function (`sc_ai_*`): its - * body on the caller's stack between scr_async_inline_enter/exit, as in - * its spawn wrapper, returning the result and leaving a throw pending for - * the awaiting coroutine (emitCoroInlineAwait). */ + * body on the caller's stack (between scr_async_inline_enter/exit when + * fiberlessFrames), as in its spawn wrapper, returning the result and + * leaving a throw pending for the awaiting coroutine (emitCoroInlineAwait). */ emitAsyncInlineAwaitCall(fn: IrFunction): string[] { const retTy = this.llType(fn.returnType); const params = fn.params.map((p, i) => `${this.llType(p.type)} %a${i}`); const args = params.join(", "); - this.declare(`declare void @scr_async_inline_enter(ptr)`); - this.declare(`declare void @scr_async_inline_exit(ptr)`); + const framed = this.fiberlessFrames; + if (framed) { + this.declare(`declare void @scr_async_inline_enter(ptr)`); + this.declare(`declare void @scr_async_inline_exit(ptr)`); + } const bodyCall = `call ${retTy} @${mangleFunction(fn.name)}(${args})`; return [ `define internal ${retTy} @${mangleAsyncInlineAwaitCall(fn.name)}(${args}) ${FN_ATTRS} { ; inline await ${fn.name}`, `entry:`, - ` %frame = alloca { ptr, ptr }`, - ` call void @scr_async_inline_enter(ptr %frame)`, + ...(framed + ? [` %frame = alloca { ptr, ptr }`, ` call void @scr_async_inline_enter(ptr %frame)`] + : []), retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`, - ` call void @scr_async_inline_exit(ptr %frame)`, + ...(framed ? [` call void @scr_async_inline_exit(ptr %frame)`] : []), retTy === "void" ? ` ret void` : ` ret ${retTy} %r`, `}`, ``, diff --git a/packages/compiler/test/await-call-emission.test.ts b/packages/compiler/test/await-call-emission.test.ts index b3867c928b..b206b829b5 100644 --- a/packages/compiler/test/await-call-emission.test.ts +++ b/packages/compiler/test/await-call-emission.test.ts @@ -62,7 +62,9 @@ test("awaiting a direct call to a body that never suspends skips the promise", a expect(definition(llvm, "sc_f_middle")).toContain("call double @sc_aw_leaf(double"); const entry = definition(llvm, "sc_aw_leaf"); expect(entry).toContain("call double @sc_f_leaf(double %a0)"); - expect(entry).toContain("call void @scr_async_inline_exit(ptr %frame)"); + // Nothing here enters an AsyncLocalStorage store or reads a stack, so the + // body runs with a plain call (fiberlessFrames). + expect(entry).not.toContain("@scr_async_inline_enter"); // The clean result takes the await's microtask turn and returns directly; // only the throw path builds the rejected promise. const clean = entry.slice(entry.indexOf("clean:"), entry.indexOf("thrown:")); @@ -122,10 +124,10 @@ test("returning a direct call adopts its promise one microtask turn later", asyn // A body that never suspends: the adoption's then-job turn, then the // settled promise's own turn, still without materializing the promise. const entry = definition(llvm, "sc_ad_leaf"); - const exit = entry.indexOf("call void @scr_async_inline_exit(ptr %frame)"); + const body = entry.indexOf("call double @sc_f_leaf(double %a0)"); const hop = entry.indexOf("call void @scr_await_hop()"); - expect(exit).toBeGreaterThan(0); - expect(hop).toBeGreaterThan(exit); + expect(body).toBeGreaterThan(0); + expect(hop).toBeGreaterThan(body); expect(hop).toBeLessThan(entry.indexOf("clean:")); const clean = entry.slice(entry.indexOf("clean:"), entry.indexOf("thrown:")); expect(clean).toContain("call void @scr_await_hop()"); @@ -190,7 +192,7 @@ test("bodies that suspend only at their own awaits compile to stackless coroutin expect(middle).toContain("call double @sc_ai_leaf(double"); const inline = definition(llvm, "sc_ai_leaf"); expect(inline).toContain("call double @sc_f_leaf(double %a0)"); - expect(inline).toContain("call void @scr_async_inline_exit(ptr %frame)"); + expect(inline).not.toContain("@scr_async_inline_enter"); expect(middle).toContain("call zeroext i1 @scr_coro_hop_prepare(ptr"); expect(middle).toContain("call void @scr_promise_reject_pending(ptr"); expect(middle).toContain("call void @scr_coro_async_finish(ptr"); @@ -205,6 +207,58 @@ test("bodies that suspend only at their own awaits compile to stackless coroutin const entry = definition(llvm, "sc_aw_middle"); expect(entry).toContain("call ptr @sc_as_middle(double %a0)"); expect(entry).toContain("call double @scr_await_f64(ptr %p)"); - // A body that never suspends stays fiberless. - expect(definition(llvm, "sc_as_leaf")).toContain("@scr_async_inline_enter("); + // A body that never suspends stays fiberless: its spawn wrapper calls the + // body and settles a fresh promise. + const spawn = definition(llvm, "sc_as_leaf"); + expect(spawn).toContain("call double @sc_f_leaf(double %a0)"); + expect(spawn).toContain("call ptr @scr_promise_new()"); + expect(spawn).not.toContain("@scr_async_spawn"); +}); + +const framedSources = { + // Entering a store makes the frame isolate the AsyncLocalStorage context. + context: ` +import { AsyncLocalStorage } from "node:async_hooks"; +const store = new AsyncLocalStorage(); +async function leaf(x: number): Promise { + store.enterWith("leaf"); + return x + 1; +}`, + // Reading a stack makes the frame isolate the Error.stack frame chain. + stack: ` +async function leaf(x: number): Promise { + if (x < 0) console.log(new Error("negative").stack); + return x + 1; +}`, +}; + +test("fiberless bodies keep their frame where it isolates a context or a stack", async () => { + for (const [name, prefix] of Object.entries(framedSources)) { + const llvm = await emit(`${prefix} +async function slow(x: number): Promise { + await null; + return x; +} +async function coroutine(x: number): Promise { + return (await slow(x)) + (await leaf(x)); +} +async function* tick(): AsyncGenerator { + yield 0; +} +async function fibered(x: number): Promise { + for await (const t of tick()) x += t; + return await leaf(x); +} +async function main(): Promise { + const pending = leaf(1); + console.log((await coroutine(2)) + (await fibered(3)) + (await pending)); +} +void main(); +`); + for (const symbol of ["sc_ai_leaf", "sc_aw_leaf", "sc_as_leaf"]) { + expect(definition(llvm, symbol), `${name} ${symbol}`).toContain( + "call void @scr_async_inline_enter(ptr %frame)", + ); + } + } }); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6953-fiberless-plain-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6953-fiberless-plain-calls.ts.json new file mode 100644 index 0000000000..2ac34ff943 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6953-fiberless-plain-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6953-fiberless-plain-calls.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6953-fiberless-plain-calls.ts b/tests/corpus/6953-fiberless-plain-calls.ts new file mode 100644 index 0000000000..68b96e984c --- /dev/null +++ b/tests/corpus/6953-fiberless-plain-calls.ts @@ -0,0 +1,65 @@ +// Never-suspending async functions in a program that enters no +// AsyncLocalStorage store and reads no stacks: awaited from stackless and +// fiber bodies, called without an await, nested, throwing, and rejecting +// without a handler, against concurrent microtasks. +const log: string[] = []; + +async function leaf(n: number): Promise { + log.push(`leaf ${n}`); + return n + 1; +} +async function nestedLeaf(n: number): Promise { + const inner = leaf(n * 10); + void inner.then((v) => log.push(`nestedLeaf inner ${v}`)); + log.push(`nestedLeaf ${n}`); + return n; +} +async function thrower(n: number): Promise { + log.push(`thrower ${n}`); + if (n > 0) throw new TypeError(`bad ${n}`); + return n; +} +function ticker(name: string, turns: number): Promise { + let p = Promise.resolve(); + for (let i = 0; i < turns; i++) p = p.then(() => void log.push(`${name} tick ${i}`)); + return p; +} + +async function stackless(): Promise { + const a = await leaf(1); + const b = await leaf(a); + try { + await thrower(b); + } catch (e) { + log.push(`stackless caught ${(e as Error).name} ${(e as Error).message}`); + } + log.push(`stackless ${a + b}`); +} +async function* source(): AsyncGenerator { + yield 1; + yield 2; +} +async function fibered(): Promise { + let s = 0; + for await (const v of source()) s += await leaf(v); + try { + s += await thrower(5); + } catch (e) { + log.push(`fibered caught ${(e as Error).message}`); + } + log.push(`fibered ${s}`); +} +async function spawned(): Promise { + const p = leaf(7); + const q = thrower(0); + log.push("spawned before await"); + log.push(`spawned ${(await p) + (await q)}`); +} + +process.on("unhandledRejection", (reason) => console.log(`unhandled ${(reason as Error).message}`)); +async function main(): Promise { + void thrower(9); + await Promise.all([ticker("t", 6), stackless(), fibered(), spawned(), nestedLeaf(3)]); + for (const line of log) console.log(line); +} +void main(); diff --git a/tests/harness/corpus-costs/6953-fiberless-plain-calls.ts.json b/tests/harness/corpus-costs/6953-fiberless-plain-calls.ts.json new file mode 100644 index 0000000000..93470e6d4c --- /dev/null +++ b/tests/harness/corpus-costs/6953-fiberless-plain-calls.ts.json @@ -0,0 +1 @@ +{ "plain": 3400, "san": 4500 } From c36c414065bdf07d6fb3f7a245943a5d8f1ac4f9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 22:23:04 -0500 Subject: [PATCH 046/395] Re-export only data shapes from package declarations, through imports and .mjs entries - A local `export { Kind }` of a name the declaration file imports (`import type { Kind } from "./kinds"`) now resolves like `export { Kind } from "./kinds"`; it was dropped. - The typedefs now reach a runtime entry outside the .js/.cjs rewrite path (an .mjs file). - Only data shapes are re-exported: an interface, type alias or enum with no method, call or construct signature, function type or other computed type, whose referenced types are data shapes too. An interface with methods describes the package's JavaScript objects and stays as the JavaScript types it (@opentelemetry/api's Counter). - NPM_STATIC_TYPES_SUFFIX moves to npm-static.ts, so that module does not import the syntax modules (the self-hosted frontend tests compile it natively). --- .../frontend/npm-static-type-exports-host.ts | 79 ++++++++--- .../frontend/npm-static-type-exports.test.ts | 53 +++++++- .../src/frontend/npm-static-type-exports.ts | 127 ++++++++++++++++-- packages/compiler/src/frontend/npm-static.ts | 29 ++-- .../main.ts.json | 6 + .../main.ts | 19 +++ .../node_modules/row-module/lib/index.d.ts | 5 + .../node_modules/row-module/lib/index.mjs | 7 + .../node_modules/row-module/package.json | 1 + .../node_modules/shape-kinds/lib/index.d.ts | 4 + .../node_modules/shape-kinds/lib/index.js | 8 ++ .../node_modules/shape-kinds/lib/kinds.d.ts | 5 + .../node_modules/shape-kinds/lib/kinds.js | 2 + .../node_modules/shape-kinds/package.json | 1 + .../main.ts.json | 1 + tests/harness/coverage.test.ts | 13 ++ 16 files changed, 320 insertions(+), 40 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7004-package-declared-type-reexports/main.ts.json create mode 100644 tests/corpus/7004-package-declared-type-reexports/main.ts create mode 100644 tests/corpus/7004-package-declared-type-reexports/node_modules/row-module/lib/index.d.ts create mode 100644 tests/corpus/7004-package-declared-type-reexports/node_modules/row-module/lib/index.mjs create mode 100644 tests/corpus/7004-package-declared-type-reexports/node_modules/row-module/package.json create mode 100644 tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/index.d.ts create mode 100644 tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/index.js create mode 100644 tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/kinds.d.ts create mode 100644 tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/kinds.js create mode 100644 tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/package.json create mode 100644 tests/harness/corpus-costs/7004-package-declared-type-reexports/main.ts.json diff --git a/packages/compiler/src/frontend/npm-static-type-exports-host.ts b/packages/compiler/src/frontend/npm-static-type-exports-host.ts index 809464763c..3c196e6a77 100644 --- a/packages/compiler/src/frontend/npm-static-type-exports-host.ts +++ b/packages/compiler/src/frontend/npm-static-type-exports-host.ts @@ -5,10 +5,10 @@ import { dirname, relative, resolve as resolvePath } from "node:path"; import { trackedFileExists, trackedReadFile } from "./input-tracker.js"; import { resolveBareModule } from "./resolve.js"; import { isJsSourceFileName } from "./tsc-codes.js"; +import { NPM_STATIC_TYPES_SUFFIX } from "./npm-static.js"; import type { FrontendServices } from "./services.js"; import * as ts from "./ts7/syntax.js"; import { - NPM_STATIC_TYPES_SUFFIX, npmStaticDeclarationCopy, npmStaticDeclarationExports, npmStaticDeclarationSpecifiers, @@ -106,28 +106,37 @@ export function npmStaticTypeExportPlan( ); for (const target of targets.values()) copy(target); }; - // The exported type-only declarations of `file`, through its re-exports. + const exportsOf = new Map(); + const declarationExports = (file: string): NpmStaticDeclarationExports | null => { + let ex = exportsOf.get(file); + if (ex === undefined) { + const sf = parse(file); + ex = sf === null ? null : npmStaticDeclarationExports(sf); + exportsOf.set(file, ex); + } + return ex; + }; + // The exported type-only declarations of `file`, through its re-exports, + // each with the file that declares it. + type Found = { decl: NpmStaticTypeDeclaration; file: string }; const typesOf = ( file: string, seen: Set, - ): { types: Map; values: Set } => { - const result = { - types: new Map(), - values: new Set(), - }; + ): { types: Map; values: Set } => { + const result = { types: new Map(), values: new Set() }; if (seen.has(file)) return result; seen.add(file); - const sf = parse(file); - if (sf === null) return result; - const own: NpmStaticDeclarationExports = npmStaticDeclarationExports(sf); - for (const t of own.types) result.types.set(t.name, t); + const own = declarationExports(file); + if (own === null) return result; + for (const t of own.types) result.types.set(t.name, { decl: t, file }); for (const v of own.values) result.values.add(v); for (const n of own.named) { const target = n.specifier === null ? null : declarationTargetOf(file, n.specifier); if (target === null) continue; const inner = typesOf(target, new Set(seen)); const t = inner.types.get(n.local); - if (t !== undefined) result.types.set(n.exported, { ...t, name: n.exported }); + if (t !== undefined) + result.types.set(n.exported, { ...t, decl: { ...t.decl, name: n.exported } }); else if (inner.values.has(n.local)) result.values.add(n.exported); } for (const specifier of own.stars) { @@ -139,6 +148,39 @@ export function npmStaticTypeExportPlan( } return result; }; + // Whether `name`, as `file` sees it, is a data shape: an interface, type + // alias or enum whose syntax describes no behavior (npm-static-type- + // exports.ts) and whose referenced types are data shapes too. A name the + // package's declaration files do not declare is a global (`Array`, + // `Record`, `Date`); a class, a value or an import from another package + // is not a data shape. + const dataMemo = new Map(); + const isData = (file: string, name: string, visiting: Set): boolean => { + const key = `${file}\0${name}`; + const memo = dataMemo.get(key); + if (memo !== undefined) return memo; + if (visiting.has(key)) return true; // a recursive shape + visiting.add(key); + const answer = ((): boolean => { + const ex = declarationExports(file); + if (ex === null) return false; + const local = ex.locals.get(name); + if (local !== undefined) + return !local.behavior && local.references.every((r) => isData(file, r, visiting)); + if (ex.valueNames.has(name)) return false; + const from = ex.imports.get(name); + if (from !== undefined) { + const target = declarationTargetOf(file, from.specifier); + return target !== null && isData(target, from.local, visiting); + } + const reexport = typesOf(file, new Set()).types.get(name); + if (reexport !== undefined) return isData(reexport.file, reexport.decl.declared, visiting); + return true; + })(); + visiting.delete(key); + dataMemo.set(key, answer); + return answer; + }; for (const pkg of packages) { const declared = resolveBareModule(entryPath, pkg, "types-only"); if (declared === null || declared.packageName !== pkg || !declared.typesFile.endsWith(".d.ts")) @@ -149,9 +191,16 @@ export function npmStaticTypeExportPlan( if (runtimeText === null) continue; const { types, values } = typesOf(declared.typesFile, new Set()); const declaredByRuntime = runtimeTypeNames(services, runtime.typesFile, runtimeText); - const exported = [...types.values()].filter( - (t) => !values.has(t.name) && !declaredByRuntime.has(t.name), - ); + // Only data shapes: a type with methods describes the package's + // JavaScript objects, which stay as the JavaScript types them. + const exported = [...types.values()] + .filter( + ({ decl, file }) => + !values.has(decl.name) && + !declaredByRuntime.has(decl.name) && + isData(file, decl.declared, new Set()), + ) + .map(({ decl }) => decl); if (exported.length === 0) continue; copy(declared.typesFile); const specifier = specifierFor(runtime.typesFile, copyPathOf(declared.typesFile)); diff --git a/packages/compiler/src/frontend/npm-static-type-exports.test.ts b/packages/compiler/src/frontend/npm-static-type-exports.test.ts index fa95c0fe08..89adf5bbce 100644 --- a/packages/compiler/src/frontend/npm-static-type-exports.test.ts +++ b/packages/compiler/src/frontend/npm-static-type-exports.test.ts @@ -40,6 +40,41 @@ export { helper };`), expect(exports.stars).toEqual(["./paging"]); }); +test("a local export of an imported name resolves like a re-export", () => { + const exports = npmStaticDeclarationExports( + parse(`import type { Kind } from "./kinds"; +import { type Shape as Form, helper } from "./kinds"; +export { Kind, Form as Outline, helper };`), + ); + expect(exports.types).toEqual([]); + expect(exports.named).toEqual([ + { exported: "Kind", local: "Kind", specifier: "./kinds" }, + { exported: "Outline", local: "Shape", specifier: "./kinds" }, + { exported: "helper", local: "helper", specifier: "./kinds" }, + ]); +}); + +test("each declaration records whether it describes behavior and what it references", () => { + const { locals } = npmStaticDeclarationExports( + parse(`interface Row { key: string; value: T; next?: Row; tags: Tag[] } +type Tag = "a" | "b"; +interface Counter { add(value: number): void } +type Callback = (row: Row) => void; +type Keys = keyof Row; +interface Named extends Base { label: Label }`), + ); + const shape = (name: string) => { + const d = locals.get(name)!; + return [d.behavior, [...d.references].sort()]; + }; + expect(shape("Row")).toEqual([false, ["Row", "Tag"]]); + expect(shape("Tag")).toEqual([false, []]); + expect(shape("Counter")).toEqual([true, []]); + expect(shape("Callback")[0]).toBe(true); + expect(shape("Keys")[0]).toBe(true); + expect(shape("Named")).toEqual([false, ["Base", "Label"]]); +}); + test("a copy respells the relative specifiers it is told", () => { const sf = parse(`import type { Page } from "./paging"; export * from './entries'; @@ -60,8 +95,22 @@ test("the typedef block re-exports each type with its parameters", () => { expect( npmStaticTypedefBlock( [ - { name: "Mode", params: [], templates: [] }, - { name: "Options", params: ["T", "U"], templates: ["{object} [T={}]", "[U=string]"] }, + { + name: "Mode", + declared: "Mode", + params: [], + templates: [], + behavior: false, + references: [], + }, + { + name: "Options", + declared: "Options", + params: ["T", "U"], + templates: ["{object} [T={}]", "[U=string]"], + behavior: false, + references: [], + }, ], "./index.scriptc-types", ), diff --git a/packages/compiler/src/frontend/npm-static-type-exports.ts b/packages/compiler/src/frontend/npm-static-type-exports.ts index bdea8eb033..3b74b8aefe 100644 --- a/packages/compiler/src/frontend/npm-static-type-exports.ts +++ b/packages/compiler/src/frontend/npm-static-type-exports.ts @@ -19,15 +19,21 @@ import * as ts from "./ts7/syntax.js"; import { tokenStartOfNode } from "./ts7/node-functions.js"; -/** The suffix that replaces `.d.ts` in a copy's name. */ -export const NPM_STATIC_TYPES_SUFFIX = ".scriptc-types.d.ts"; - -/** A type-only declaration: its name, its type parameters' names and their - * JSDoc @template tag bodies. */ +/** A type-only declaration: its exported name, its name where it is + * declared, its type parameters' names and their JSDoc @template tag + * bodies, and what npmStaticDataShape needs to know. */ export interface NpmStaticTypeDeclaration { name: string; + declared: string; params: readonly string[]; templates: readonly string[]; + /** The declaration's own syntax describes behavior or a computed type: + * a method, call or construct signature, a function or constructor + * type, an accessor, `typeof`, `import("...")`, a conditional, mapped, + * indexed-access or `keyof` type, or a qualified type name. */ + behavior: boolean; + /** The type names it refers to, its own type parameters excepted. */ + references: readonly string[]; } /** What a declaration file declares and re-exports, by exported name. */ @@ -37,11 +43,17 @@ export interface NpmStaticDeclarationExports { /** Exported classes, functions, variables and namespaces: names the * JavaScript declares itself, or that are not type-only. */ values: readonly string[]; - /** `export { local as exported } from "specifier"` and local - * `export { a, b }` (specifier null). */ + /** `export { local as exported } from "specifier"`, including a local + * `export { a }` of an imported name. */ named: readonly { exported: string; local: string; specifier: string | null }[]; /** `export * from "specifier"`. */ stars: readonly string[]; + /** Every interface, type alias and enum the file declares, by name. */ + locals: ReadonlyMap; + /** The file's named imports: local name to imported name and specifier. */ + imports: ReadonlyMap; + /** Every class, function, namespace and variable the file declares. */ + valueNames: ReadonlySet; } function isExported(node: ts.Node): boolean { @@ -60,6 +72,49 @@ function templatesOf(sf: ts.SourceFile, params: readonly ts.TypeParameterDeclara }); } +/** Syntax kinds of a type that describes behavior or is computed. */ +const BEHAVIOR_KINDS: ReadonlySet = new Set([ + ts.SyntaxKind.MethodSignature, + ts.SyntaxKind.CallSignature, + ts.SyntaxKind.ConstructSignature, + ts.SyntaxKind.FunctionType, + ts.SyntaxKind.ConstructorType, + ts.SyntaxKind.GetAccessor, + ts.SyntaxKind.SetAccessor, + ts.SyntaxKind.TypeQuery, + ts.SyntaxKind.ImportType, + ts.SyntaxKind.ConditionalType, + ts.SyntaxKind.MappedType, + ts.SyntaxKind.InferType, + ts.SyntaxKind.IndexedAccessType, + ts.SyntaxKind.ThisType, + ts.SyntaxKind.TypePredicate, + ts.SyntaxKind.TemplateLiteralType, +]); + +/** The behavior flag and type references of an interface or type alias. */ +function shapeOf( + decl: ts.InterfaceDeclaration | ts.TypeAliasDeclaration, + params: readonly string[], +): { behavior: boolean; references: string[] } { + let behavior = false; + const references = new Set(); + const refer = (name: ts.Node): void => { + if (ts.isIdentifier(name)) { + if (!params.includes(name.text)) references.add(name.text); + } else behavior = true; // a qualified name: a namespace member + }; + ts.walkPreorder(decl, (n) => { + if (behavior) return "stop"; + if (BEHAVIOR_KINDS.has(n.kind)) behavior = true; + else if (ts.isTypeOperatorNode(n) && n.operator === ts.SyntaxKind.KeyOfKeyword) behavior = true; + else if (ts.isTypeReferenceNode(n)) refer(n.typeName); + else if (ts.isExpressionWithTypeArguments(n)) refer(n.expression); + return undefined; + }); + return { behavior, references: [...references] }; +} + /** The exports of one declaration file. */ export function npmStaticDeclarationExports(sf: ts.SourceFile): NpmStaticDeclarationExports { const types: NpmStaticTypeDeclaration[] = []; @@ -68,19 +123,44 @@ export function npmStaticDeclarationExports(sf: ts.SourceFile): NpmStaticDeclara const stars: string[] = []; const localTypes = new Map(); const localValues = new Set(); + // `import { A as B } from "./x"`: local name B is A of "./x". + const imported = new Map(); for (const stmt of sf.statements.nodes) { const exported = isExported(stmt); - if (ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)) { + if (ts.isImportDeclaration(stmt)) { + const bindings = stmt.importClause?.namedBindings; + if (ts.isStringLiteral(stmt.moduleSpecifier) && bindings && ts.isNamedImports(bindings)) { + for (const e of bindings.elements.nodes) { + imported.set(e.name.text, { + local: (e.propertyName ?? e.name).text, + specifier: stmt.moduleSpecifier.text, + }); + } + } + } else if (ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)) { const params = stmt.typeParameters?.nodes ?? []; - const decl = { + const paramNames = params.map((p) => p.name.text); + const shape = shapeOf(stmt, paramNames); + const merged = localTypes.get(stmt.name.text); // interface merging + const decl: NpmStaticTypeDeclaration = { name: stmt.name.text, - params: params.map((p) => p.name.text), + declared: stmt.name.text, + params: paramNames, templates: templatesOf(sf, params), + behavior: shape.behavior || merged?.behavior === true, + references: [...shape.references, ...(merged?.references ?? [])], }; localTypes.set(decl.name, decl); - if (exported) types.push(decl); + if (exported && merged === undefined) types.push(decl); } else if (ts.isEnumDeclaration(stmt)) { - const decl: NpmStaticTypeDeclaration = { name: stmt.name.text, params: [], templates: [] }; + const decl: NpmStaticTypeDeclaration = { + name: stmt.name.text, + declared: stmt.name.text, + params: [], + templates: [], + behavior: false, + references: [], + }; localTypes.set(decl.name, decl); if (exported) types.push(decl); } else if ( @@ -116,7 +196,9 @@ export function npmStaticDeclarationExports(sf: ts.SourceFile): NpmStaticDeclara } } } - // Local `export { a as b }` of the file's own declarations. + // Local `export { a as b }`: of the file's own declarations, or of a + // name it imports (`import type { A } from "./a"; export { A }`), which + // resolves like `export { A } from "./a"`. const resolvedNamed: typeof named = []; for (const n of named) { if (n.specifier !== null) { @@ -124,10 +206,27 @@ export function npmStaticDeclarationExports(sf: ts.SourceFile): NpmStaticDeclara continue; } const local = localTypes.get(n.local); + const from = imported.get(n.local); if (local !== undefined) types.push({ ...local, name: n.exported }); else if (localValues.has(n.local)) values.push(n.exported); + else if (from !== undefined) resolvedNamed.push({ exported: n.exported, ...from }); } - return { types, values, named: resolvedNamed, stars }; + // A merged interface's exported entry takes the merged flags. + const exportedTypes: NpmStaticTypeDeclaration[] = []; + for (const t of types) { + const local = localTypes.get(t.declared); + if (local === undefined) exportedTypes.push(t); + else exportedTypes.push({ ...local, name: t.name }); + } + return { + types: exportedTypes, + values, + named: resolvedNamed, + stars, + locals: localTypes, + imports: imported, + valueNames: localValues, + }; } /** The relative module specifiers of a declaration file's imports, exports diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index 33ff54cfd5..800d02effb 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -23,7 +23,6 @@ import { trackedRealpath, } from "./input-tracker.js"; import { npmBuiltinAliasOf } from "./builtin-modules.js"; -import { NPM_STATIC_TYPES_SUFFIX } from "./npm-static-type-exports.js"; let activePackages: ReadonlySet = new Set(); let declarationOverloads: ReadonlyMap = new Map(); @@ -55,6 +54,11 @@ export function npmStaticLexerVisibleNames(path: string): readonly string[] { return lexerNames.get(path.split("\\").join("/")) ?? []; } +/** The suffix that replaces `.d.ts` in the name of a copy of an opted-in + * package's declaration file (npm-static-type-exports.ts). Defined here so + * this module does not depend on the syntax modules. */ +export const NPM_STATIC_TYPES_SUFFIX = ".scriptc-types.d.ts"; + /** Per runtime entry (forward slashes), the JSDoc typedefs that re-export * its package's type-only declarations (npm-static-type-exports.ts). */ let typeExportTypedefs = new Map(); @@ -71,6 +75,17 @@ export function setNpmStaticTypeExports( rewriteCache.clear(); } +/** `text` (or the file's own text when null) with the typedefs of a + * package runtime entry appended: the entry re-exports the type-only + * declarations of the package's .d.ts, after every respelling, so + * positions stay. Null when `path` is no runtime entry and `text` is null. */ +function withTypeExportTypedefs(path: string, text: string | null): string | null { + const typedefs = typeExportTypedefs.get(path.split("\\").join("/")); + if (typedefs === undefined) return text; + const source = text ?? trackedReadFile(path); + return source === null ? text : `${source}\n${typedefs}\n`; +} + export function setNpmStaticPackages>(packages: T): void { // npm packages served as builtins (graceful-fs as fs) never compile // their JavaScript, even when named. @@ -543,17 +558,13 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow } catch { rewritten = null; // unreadable/unparseable: fall through untouched } - // The runtime entry re-exports the type-only declarations of the - // package's .d.ts, after every respelling, so positions stay. - const typedefs = typeExportTypedefs.get(path.split("\\").join("/")); - if (typedefs !== undefined) { - const text = rewritten ?? trackedReadFile(path); - if (text !== null) rewritten = `${text}\n${typedefs}\n`; - } + rewritten = withTypeExportTypedefs(path, rewritten); rewriteCache.set(path, rewritten); return rewritten ?? undefined; } - return undefined; + // Other runtime entries (`.mjs`) keep their text and gain only the + // typedefs. + return withTypeExportTypedefs(path, null) ?? undefined; }, hideFile: (path) => { // The copies of the package's declaration files the typedefs name. diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7004-package-declared-type-reexports/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7004-package-declared-type-reexports/main.ts.json new file mode 100644 index 0000000000..927b902d99 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7004-package-declared-type-reexports/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7004-package-declared-type-reexports/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7004-package-declared-type-reexports/main.ts b/tests/corpus/7004-package-declared-type-reexports/main.ts new file mode 100644 index 0000000000..52e45e7d4d --- /dev/null +++ b/tests/corpus/7004-package-declared-type-reexports/main.ts @@ -0,0 +1,19 @@ +// Two packages that compile from their JavaScript, with types only their +// .d.ts declares: one re-exports types its entry imports +// (`import type { Kind } from "./kinds"; export { Kind }`), the other's +// runtime entry is an ES module (`.mjs`). Declarations using those types +// keep TypeScript's types: the empty arrays the packages fill hold the +// declared rows. +import { addShapes, type Form, type Kind } from "shape-kinds"; +import { readRows, type Row } from "row-module"; + +const forms: Form[] = []; +addShapes("c2 s3", forms); +const kinds: Kind[] = forms.map((form) => form.kind); +const first = forms[0]!; +console.log(kinds.join(","), first.size + forms[1]!.size, first.kind === "circle"); + +const rows: Row[] = []; +const count = readRows("a=1;b=22", rows); +const last = rows[count - 1]!; +console.log(count, last.key, last.value.length); diff --git a/tests/corpus/7004-package-declared-type-reexports/node_modules/row-module/lib/index.d.ts b/tests/corpus/7004-package-declared-type-reexports/node_modules/row-module/lib/index.d.ts new file mode 100644 index 0000000000..1f732f9966 --- /dev/null +++ b/tests/corpus/7004-package-declared-type-reexports/node_modules/row-module/lib/index.d.ts @@ -0,0 +1,5 @@ +export interface Row { + key: string; + value: string; +} +export declare function readRows(text: string, into: Row[]): number; diff --git a/tests/corpus/7004-package-declared-type-reexports/node_modules/row-module/lib/index.mjs b/tests/corpus/7004-package-declared-type-reexports/node_modules/row-module/lib/index.mjs new file mode 100644 index 0000000000..33d110065d --- /dev/null +++ b/tests/corpus/7004-package-declared-type-reexports/node_modules/row-module/lib/index.mjs @@ -0,0 +1,7 @@ +export function readRows(text, into) { + for (const part of text.split(";")) { + const at = part.indexOf("="); + into.push({ key: part.slice(0, at), value: part.slice(at + 1) }); + } + return into.length; +} diff --git a/tests/corpus/7004-package-declared-type-reexports/node_modules/row-module/package.json b/tests/corpus/7004-package-declared-type-reexports/node_modules/row-module/package.json new file mode 100644 index 0000000000..240a3f8162 --- /dev/null +++ b/tests/corpus/7004-package-declared-type-reexports/node_modules/row-module/package.json @@ -0,0 +1 @@ +{ "name": "row-module", "version": "1.0.0", "main": "./lib/index.mjs", "types": "./lib/index.d.ts" } diff --git a/tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/index.d.ts b/tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/index.d.ts new file mode 100644 index 0000000000..ada1748d85 --- /dev/null +++ b/tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/index.d.ts @@ -0,0 +1,4 @@ +import type { Kind } from "./kinds"; +import { type Shape as Form } from "./kinds"; +export { Kind, Form }; +export declare function addShapes(text: string, into: Form[]): void; diff --git a/tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/index.js b/tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/index.js new file mode 100644 index 0000000000..d6165c4b9e --- /dev/null +++ b/tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/index.js @@ -0,0 +1,8 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.addShapes = addShapes; +function addShapes(text, into) { + for (const part of text.split(" ")) { + into.push({ kind: part.charAt(0) === "c" ? "circle" : "square", size: Number(part.slice(1)) }); + } +} diff --git a/tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/kinds.d.ts b/tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/kinds.d.ts new file mode 100644 index 0000000000..79c90322ce --- /dev/null +++ b/tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/kinds.d.ts @@ -0,0 +1,5 @@ +export type Kind = "circle" | "square"; +export interface Shape { + kind: Kind; + size: number; +} diff --git a/tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/kinds.js b/tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/kinds.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/lib/kinds.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/package.json b/tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/package.json new file mode 100644 index 0000000000..e428fc3159 --- /dev/null +++ b/tests/corpus/7004-package-declared-type-reexports/node_modules/shape-kinds/package.json @@ -0,0 +1 @@ +{ "name": "shape-kinds", "version": "1.0.0", "main": "./lib/index.js", "types": "./lib/index.d.ts" } diff --git a/tests/harness/corpus-costs/7004-package-declared-type-reexports/main.ts.json b/tests/harness/corpus-costs/7004-package-declared-type-reexports/main.ts.json new file mode 100644 index 0000000000..e9e0bf6d21 --- /dev/null +++ b/tests/harness/corpus-costs/7004-package-declared-type-reexports/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1015, "san": 2516 } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index ca62352b11..1f790c2259 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -488,6 +488,19 @@ test("JavaScript builtin aliases keep their deferred value fence", async () => { ); }); +test("types only a package's .d.ts declares stay static through re-exports and .mjs entries", () => { + // shape-kinds re-exports types its .d.ts entry imports; row-module's + // runtime entry is an .mjs file. Without their declared types the + // program's uses of Form, Kind and Row run in the dynamic tier (12 + // statements); with them only the packages' own JavaScript and one + // arithmetic on a JavaScript return value do (4). + const { coverage } = analyze( + join(repoRoot, "tests/corpus/7004-package-declared-type-reexports/main.ts"), + ); + expect(coverage.diagnostics).toEqual([]); + expect(coverage.stats.statementsDynamic).toBe(4); +}); + test("any-typed checked-dynamic operators compile natively", () => { const file = join(repoRoot, "tests/corpus/2856-dynamic-any-local-operators.ts"); const staticCoverage = analyze(file).coverage; From 3e8f1bc608820219f71dcb14463d8d665e6bc1a5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 22:34:17 -0500 Subject: [PATCH 047/395] Renumber the fiberless plain-call corpus program to 7103 --- .../tests/corpus/6953-fiberless-plain-calls.ts.json | 6 ------ .../tests/corpus/7103-fiberless-plain-calls.ts.json | 6 ++++++ ...berless-plain-calls.ts => 7103-fiberless-plain-calls.ts} | 0 ...ain-calls.ts.json => 7103-fiberless-plain-calls.ts.json} | 0 4 files changed, 6 insertions(+), 6 deletions(-) delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6953-fiberless-plain-calls.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7103-fiberless-plain-calls.ts.json rename tests/corpus/{6953-fiberless-plain-calls.ts => 7103-fiberless-plain-calls.ts} (100%) rename tests/harness/corpus-costs/{6953-fiberless-plain-calls.ts.json => 7103-fiberless-plain-calls.ts.json} (100%) diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6953-fiberless-plain-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6953-fiberless-plain-calls.ts.json deleted file mode 100644 index 2ac34ff943..0000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6953-fiberless-plain-calls.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/6953-fiberless-plain-calls.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7103-fiberless-plain-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7103-fiberless-plain-calls.ts.json new file mode 100644 index 0000000000..80fe9b85fc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7103-fiberless-plain-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7103-fiberless-plain-calls.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6953-fiberless-plain-calls.ts b/tests/corpus/7103-fiberless-plain-calls.ts similarity index 100% rename from tests/corpus/6953-fiberless-plain-calls.ts rename to tests/corpus/7103-fiberless-plain-calls.ts diff --git a/tests/harness/corpus-costs/6953-fiberless-plain-calls.ts.json b/tests/harness/corpus-costs/7103-fiberless-plain-calls.ts.json similarity index 100% rename from tests/harness/corpus-costs/6953-fiberless-plain-calls.ts.json rename to tests/harness/corpus-costs/7103-fiberless-plain-calls.ts.json From 56880e110cc87a26809ca0250873a07ec014457b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 23:19:57 -0500 Subject: [PATCH 048/395] Keep the slots of short known-count arrays in the array's block An array constructed with a known element count of up to 16 (array literals, copies, slices and the arrays the runtime builds) keeps its slots in its own block, as empty arrays built for appends already do, so it costs one allocation and one free. scr_arr_make_ops is inlined into its two constructors, so the size class of scr_arr_new_ops_small folds to a table index. It writes the element ops id with the other header fields and records ops past the registry in the array's ext after the storage is set up: the ext pointer precedes the dense allocation, so inline slots must move out before the ext is created. --- packages/runtime/src/scr_array.c | 62 ++++++++++++++++++++++------- packages/runtime/test/test_array.c | 64 +++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 17 deletions(-) diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index c64b859478..c00d1b4f66 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -131,13 +131,6 @@ uint32_t scr_arr_ops_register(ScrArrOps *ops) { const ScrArrOps *scr_arr_ops_overflow(const ScrArr *a) { return scr_arr_ext_of(a)->ops; } -/* Record `ops` as the element ops of `a` (NULL for none). */ -static void scr_arr_set_ops(ScrArr *a, const ScrArrOps *ops) { - uint32_t id = scr_arr_ops_id(ops); - a->spare = (uint16_t)((a->spare & ((1u << SCR_ARR_OPS_SHIFT) - 1)) | (id << SCR_ARR_OPS_SHIFT)); - if (id == SCR_ARR_OPS_OVERFLOW) scr_arr_ext(a)->ops = ops; -} - /* Canonical array indices grow indexed storage and may leave holes. Negative, * fractional, NaN, infinity, and 2^32-1 numeric keys use the ordinary * property vector; only a missing property or a low-level read of a hole @@ -402,9 +395,10 @@ static void scr_arr_sparse_drop_prefix(ScrArr *a, size_t count) { #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) 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. + * (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. * `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. */ @@ -526,6 +520,30 @@ static size_t scr_arr_small_cap(size_t prefix, bool bools) { #endif } +/* The most slots a construction with a known element count (array + * literals, copies, slices and the arrays the runtime builds) keeps inline. + * Larger counts, and blocks past the small-object allocator, keep separate + * storage: an array that then grows leaves its inline room unused, which + * this bound keeps small. */ +#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) { + 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; + return need; +#endif +} + 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; @@ -1180,7 +1198,8 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind el 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) : 0; + 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); ScrArr *a = scr_mem_alloc(inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr)); if (!a) scr_arr_oom(); a->rc = 1; @@ -1310,11 +1329,17 @@ static void scr_arr_gc_free(void *a0) { scr_cyc_free_inline(a); } -static ScrArr *scr_arr_make_ops(const ScrArrOps *ops, size_t initial_cap, bool small) { +/* 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) { ScrArr *a; bool cyc = ops->trace != NULL; + size_t prefix = SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0); size_t inline_cap = - small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0), false) : 0; + 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); if (cyc) { static ScrCycType cyc_type = { &scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY }; @@ -1328,12 +1353,19 @@ static ScrArr *scr_arr_make_ops(const ScrArrOps *ops, size_t initial_cap, bool s a->cap = 0; a->elem = SCR_ELEM_REF; a->cyc = cyc; - a->spare = 0; + /* The ops id is written with the other header fields: reading `spare` + * back after scr_arr_use_inline sets its inline flag stalls the + * store-to-load forward. */ + uint32_t id = scr_arr_ops_id(ops); + a->spare = (uint16_t)(id << SCR_ARR_OPS_SHIFT); a->filled = 0; a->data = NULL; - scr_arr_set_ops(a, ops); if (inline_cap) scr_arr_use_inline(a, inline_cap); 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 + * (scr_arr_ext). */ + if (id == SCR_ARR_OPS_OVERFLOW) scr_arr_ext(a)->ops = ops; #ifdef SCR_RC_AUDIT scr_live_arrays++; #endif diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index 02da2fac24..d943acfb22 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -398,6 +398,8 @@ static MockRec *mock_cyc_new(int value) { static ScrArrOps mock_ops = { &mock_retain, &mock_release, NULL }; static ScrArrOps mock_cyc_ops = { &mock_cyc_retain, &mock_cyc_release, &mock_trace }; +/* First used after test_ops_overflow fills the registry: its arrays keep the ops in their ext. */ +static ScrArrOps mock_cyc_ops_late = { &mock_cyc_retain, &mock_cyc_release, &mock_trace }; static void test_ref_elements(void) { #ifdef SCR_RC_AUDIT @@ -514,6 +516,38 @@ static void test_ref_cycle(void) { #endif } +static void test_ref_known_count_cycle(ScrArrOps *ops) { +#ifdef SCR_RC_AUDIT + long arrays0 = scr_arr_live_count(); +#endif + /* A known-count construction of cycle-capable elements keeps its slots + * after the collector header: the collector traces them, and both the + * collected array and one released by its count free the one block. */ + ScrArr *arr = scr_arr_new_ops(ops, 2); + bool in_block = arr->data == (uint64_t *)((char *)arr + ((sizeof(ScrArr) + 7) & ~(size_t)7)); + /* Ops past the registry (once test_ops_overflow has filled it) live in + * the ext, which moves the slots to their own allocation. */ + bool overflow = (arr->spare >> SCR_ARR_OPS_SHIFT) == SCR_ARR_OPS_OVERFLOW; + check(arr->cap >= 2 && in_block == !overflow && scr_arr_ops(arr) == ops, + "known-count cycle-capable construction is inline unless its ops need the ext"); + MockRec *first = mock_cyc_new(1); + first->owner = (ScrArr *)scr_arr_retain(arr); + scr_arr_push_ref(arr, first); + scr_arr_push_ref(arr, mock_cyc_new(2)); + scr_arr_release(arr); + scr_collect_cycles(); + check(mock_live == 0, "cycle through an inline array collected"); + ScrArr *plain = scr_arr_new_ops(ops, 3); + for (int i = 0; i < 4; i++) scr_arr_push_ref(plain, mock_cyc_new(i)); + check(scr_arr_len(plain) == 4, "a cycle-capable inline array grows out of its block"); + scr_arr_release(plain); + scr_collect_cycles(); + check(mock_live == 0, "released inline cycle-capable array frees its elements"); +#ifdef SCR_RC_AUDIT + check(scr_arr_live_count() == arrays0, "known-count cycle: no arrays leaked"); +#endif +} + static void test_ref_truncate_cycle(void) { #ifdef SCR_RC_AUDIT long arrays0 = scr_arr_live_count(); @@ -1543,9 +1577,33 @@ static void test_inline_slots(void) { long strings0 = scr_str_live_count(); long arrays0 = scr_arr_live_count(); #endif - ScrArr *plain = scr_arr_new(SCR_ELEM_F64, 4); - check(!inline_slots(plain), "plain constructions keep separate storage"); + ScrArr *plain = scr_arr_new(SCR_ELEM_F64, 0); + check(!inline_slots(plain) && plain->data == NULL, "empty plain constructions allocate no slots"); scr_arr_release(plain); + /* A construction with a known count up to 16 slots keeps them in its own + * 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, + "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); + check(!inline_slots(known) && scr_arr_len(known) == known_cap + 1 && scr_arr_get_f64(known, 0) == 0 && + scr_arr_get_f64(known, (double)known_cap) == (double)known_cap, + "growth past a known-count construction moves the slots out"); + scr_arr_release(known); + ScrArr *sixteen = scr_arr_new(SCR_ELEM_STR, 16); + for (int i = 0; i < 16; i++) scr_arr_push_ref(sixteen, scr_str_new("s", 1)); + check(inline_slots(sixteen) && scr_arr_len(sixteen) == 16, "sixteen known slots stay inline"); + scr_arr_release(sixteen); + ScrArr *large = scr_arr_new(SCR_ELEM_F64, 17); + check(!inline_slots(large) && large->cap >= 17, "larger known counts keep separate storage"); + scr_arr_release(large); + ScrArr *known_flags = scr_arr_new(SCR_ELEM_BOOL, 5); + scr_arr_push_bool(known_flags, true); + check(inline_slots(known_flags) && known_flags->cap >= 5 && scr_arr_get_bool(known_flags, 0), + "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, @@ -2205,7 +2263,9 @@ int main(int argc, char **argv) { test_nested_rc(); test_index_of_includes(); test_ref_elements(); + test_ref_known_count_cycle(&mock_cyc_ops); test_ops_overflow(); + test_ref_known_count_cycle(&mock_cyc_ops_late); test_ext_through_storage_changes(); test_ref_cycle(); test_ref_truncate_cycle(); From c22302cfae00dc5455c6089364d450c5f9164ffb Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 23:22:03 -0500 Subject: [PATCH 049/395] Test runtime classes' dispatch branches before their subclasses' --- .../lowering/class-dynamic-dispatch.ts | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index a41751f018..c90e1135b4 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -407,9 +407,27 @@ function withTypedReceiver(value: IrExpr, receiver: IrExpr): IrExpr { }); } -/** Re-adds the entries of `set` after the first `from` sorted. */ -function sortAdded(set: Set, from: number): void { - const added = [...set].slice(from).sort(); +/** Re-adds the classes of `set` after the first `from` in a fixed order: + * the program's classes by name, then the classes the runtime implements by + * name. The dispatch helpers add each class's branch in front of the + * earlier ones, so a runtime class's branch, which answers its methods for + * instances of the program's subclasses too, is tested before the + * subclasses' own branches. */ +function sortAdded(lowerer: Lowerer, set: Set, from: number): void { + const runtime = (name: string): boolean => { + const info = lowerer.classes.get(name); + return !!( + info && + (info.def.runtime || info.builtinEmitter || info.builtinStream || info.builtinError) + ); + }; + const added = [...set] + .slice(from) + .sort( + (left, right) => + Number(runtime(left)) - Number(runtime(right)) || + (left < right ? -1 : left > right ? 1 : 0), + ); for (const name of added) set.delete(name); for (const name of added) set.add(name); } @@ -932,10 +950,10 @@ export class ClassDynamicDispatch { return true; }, }); - // The classes this pass found join the tables by name, not in the order - // the scan met them. - sortAdded(this.boxed, boxedBefore); - sortAdded(this.boxedConstructors, boxedConstructorsBefore); + // The classes this pass found join the tables in a fixed order, not in + // the order the scan met them. + sortAdded(lowerer, this.boxed, boxedBefore); + sortAdded(lowerer, this.boxedConstructors, boxedConstructorsBefore); changed = this.caughtDispatch.dynamicPayloads(lowerer, caughtSites, this.boxed) || changed; if (this.boxed.size === 0 && rewrite.size === 0) return changed; const converting = this.convertingHints(lowerer); From ecee5fdaaffe061b715a73915aea39179f6d5803 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 23:25:33 -0500 Subject: [PATCH 050/395] Store references to ProgramLifetime instances in 4 bytes on Linux --- docs/content/docs/contracts.mdx | 2 + .../compiler/src/backend/llvm/classes.test.ts | 109 +++++++++- packages/compiler/src/backend/llvm/classes.ts | 144 ++++++++++++- .../src/backend/llvm/emit-function.ts | 1 + .../compiler/src/backend/llvm/emit-module.ts | 4 + .../src/backend/llvm/emit-statements.ts | 2 +- packages/compiler/src/backend/llvm/emitter.ts | 116 ++++++++-- .../compiler/src/backend/llvm/expr-control.ts | 10 +- .../compiler/src/backend/llvm/expr-dynamic.ts | 2 +- .../src/backend/llvm/expr-primitives.ts | 10 +- .../src/backend/llvm/expr-stream-bridges.ts | 39 +++- packages/compiler/src/backend/llvm/publish.ts | 17 +- .../compiler/src/backend/llvm/receivers.ts | 7 +- packages/compiler/src/index.ts | 8 +- packages/compiler/src/native/prepare.ts | 3 +- ...ntract-program-lifetime-references.ts.json | 6 + ...-contract-program-lifetime-workers.ts.json | 6 + packages/runtime/src/scr_alloc.c | 200 ++++++++++++++++++ packages/runtime/src/scr_rc.h | 24 +++ scripts/ci-test-costs.json | 2 + ...36-contract-program-lifetime-references.ts | 195 +++++++++++++++++ .../4637-contract-program-lifetime-workers.ts | 79 +++++++ ...ntract-program-lifetime-references.ts.json | 1 + ...-contract-program-lifetime-workers.ts.json | 1 + .../harness/program-lifetime-workers.test.ts | 48 +++++ 25 files changed, 981 insertions(+), 55 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4636-contract-program-lifetime-references.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4637-contract-program-lifetime-workers.ts.json create mode 100644 tests/corpus/4636-contract-program-lifetime-references.ts create mode 100644 tests/corpus/4637-contract-program-lifetime-workers.ts create mode 100644 tests/harness/corpus-costs/4636-contract-program-lifetime-references.ts.json create mode 100644 tests/harness/corpus-costs/4637-contract-program-lifetime-workers.ts.json create mode 100644 tests/harness/program-lifetime-workers.test.ts diff --git a/docs/content/docs/contracts.mdx b/docs/content/docs/contracts.mdx index d1bfb8cada..5af1cad2f1 100644 --- a/docs/content/docs/contracts.mdx +++ b/docs/content/docs/contracts.mdx @@ -349,6 +349,8 @@ 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. + scriptc checks that the promise cannot change what the program computes (SC7008): - the class is the root of its hierarchy, so every value of any class type in the hierarchy is program-lifetime; built-in bases such as `Error`, `EventEmitter` and streams are refused because they hold runtime state that is released with the object; diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index 94da472d45..2d95e942cc 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -1,13 +1,15 @@ import { expect, test } from "vitest"; import { DYN, F64, STRING, VOID, type IrModule } from "../../ir/ir.js"; import { + assignCompactLayouts, buildClassGraph, + classFieldIndex, classIdLoadLines, classMembershipIntervals, emitClassMembershipHelper, type LlClassMeta, } from "./classes.js"; -import { emitLlvmModule } from "./emitter.js"; +import { emitLlvmModule, lowRegionTarget } from "./emitter.js"; import { mangleClassNew, mangleClassReleaseDirect, @@ -196,11 +198,9 @@ test("classes whose trace visits nothing start acyclic in a traced hierarchy", ( }); test("ProgramLifetime classes pack their fields after a 32-bit header", () => { - const node = { kind: "object" as const, className: "Node" }; - const leaf = { kind: "object" as const, className: "Leaf" }; const BOOL = { kind: "bool" as const }; const nodeFields = [ - { name: "parent", type: node }, + { name: "parent", type: STRING }, { name: "flag", type: BOOL }, { name: "weight", type: F64 }, ]; @@ -214,7 +214,7 @@ test("ProgramLifetime classes pack their fields after a 32-bit header", () => { { name: "Leaf", base: "Node", - fields: [...nodeFields, { name: "next", type: leaf }, { name: "mark", type: BOOL }], + fields: [...nodeFields, { name: "next", type: STRING }, { name: "mark", type: BOOL }], methods: [], loc, }, @@ -233,6 +233,105 @@ test("ProgramLifetime classes pack their fields after a 32-bit header", () => { expect(mortal).toContain(`%${mangleClassStruct("Leaf")} = type { i64, ptr, i8, i32, ptr, i8 }`); }); +test("references between ProgramLifetime instances are 32-bit indexes into their region", () => { + const node = { kind: "object" as const, className: "Node" }; + const leaf = { kind: "object" as const, className: "Leaf" }; + const root = { localId: "root", name: "root", type: leaf }; + const nodeFields = [{ name: "parent", type: node }]; + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + functions: [ + { + name: "main", + params: [root], + returnType: VOID, + locals: [{ id: "root", name: "root", type: leaf, mutable: false }], + body: [ + { + kind: "fieldSet", + obj: { kind: "varRef", localId: "root", type: leaf, loc }, + className: "Leaf", + field: "parent", + value: { + kind: "fieldGet", + obj: { kind: "varRef", localId: "root", type: leaf, loc }, + className: "Leaf", + field: "next", + type: leaf, + loc, + }, + loc, + }, + ], + loc, + }, + ], + classes: [ + { name: "Node", fields: nodeFields, methods: [], loc }, + { + name: "Leaf", + base: "Node", + fields: [...nodeFields, { name: "next", type: leaf }, { name: "label", type: STRING }], + methods: [], + loc, + }, + ], + contracts: { programLifetime: ["Node"] }, + }; + // Linux executables without sanitizers (lowRegionTarget). + const llvm = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false, lowRegion: true }); + // parent and next take 4 bytes each; the string keeps its pointer. + expect(llvm).toContain(`%${mangleClassStruct("Node")} = type { i32, i32 }`); + expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, i32, ptr, i32 }`); + // Instances come from the region, reserved below 32 GiB before compiled + // code runs: a bump of the thread's chunk, and the runtime when it is used + // up. + const nw = + new RegExp(`^define internal ptr @${mangleClassNew("Leaf")}\\([^]*?^}`, "m").exec(llvm)?.[0] ?? + ""; + expect(nw).toContain("%plr.end = getelementptr i8, ptr %plr.next"); + expect(nw).toContain("store ptr %plr.end, ptr %plr.c"); + expect(nw).toMatch(/%plr.o = call ptr @scr_plr_alloc\(i64 /); + expect(llvm).toMatch(/call void @scr_init\(\)\n {2}call void @scr_plr_init\(\)/); + // A reference is the instance's address over 8 (0 is NULL): a read + // shifts the loaded word, a write shifts the pointer. + const main = llvm.split(/^define /m).find((fn) => fn.includes("; main\n")) ?? ""; + expect(main).toMatch(/= load i32, ptr %\S+, !tbaa/); + expect(main).toMatch(/= shl nuw nsw i64 %\S+, 3\n\s+%\S+ = inttoptr i64 %\S+ to ptr/); + expect(main).toMatch(/= ptrtoint ptr %\S+ to i64\n\s+%\S+ = lshr i64 %\S+, 3/); + expect(main).toMatch(/store i32 %\S+, ptr %\S+/); + expect(main).not.toContain("select"); + expect(lowRegionTarget("linux", false)).toBe(true); + expect(lowRegionTarget("linux", true)).toBe(false); + expect(lowRegionTarget("darwin", false)).toBe(false); + expect(lowRegionTarget("win32", false)).toBe(false); + // Other targets, 32-bit targets and mortal classes keep pointers. + const elsewhere = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false }); + expect(elsewhere).toContain(`%${mangleClassStruct("Leaf")} = type { i32, ptr, ptr, ptr }`); + expect(elsewhere).not.toContain("scr_plr"); + const llvm32 = emitLlvmModule(mod, { pointerBits: 32, lowRegion: true }); + expect(llvm32).not.toContain("scr_plr"); + const mortal = emitLlvmModule({ ...mod, contracts: {} }, { pointerBits: 64, lowRegion: true }); + expect(mortal).not.toContain("scr_plr"); + // Field accessors that read the storage as a pointer are refused. + const metas = buildClassGraph(mod, new Map()); + assignCompactLayouts( + metas, + () => true, + (cls) => cls.fields.map(() => "ptr"), + (cls, i, isCompact) => { + const t = cls.fields[i]!.type; + return t.kind === "object" && isCompact(t.className); + }, + ); + const leafMeta = metas.get("Leaf")!; + expect(() => classFieldIndex(leafMeta, "next")).toThrow(/holds a 32-bit reference/); + expect(classFieldIndex(leafMeta, "next", "compressed").compressed).toBe(true); + expect(classFieldIndex(leafMeta, "label").compressed).toBe(false); +}); + 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 7ca2216d2e..3073bd1bdc 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -98,6 +98,10 @@ export class LlClassMeta { compactElems: { offset: number; size: number; align: number; type: string; field: number }[] = []; /** Compact classes: each field's GEP index, by its index in def.fields. */ compactSlots: number[] = []; + /** Compact classes: whether each field (by its index in def.fields) holds + * a compact class instance as a 32-bit index (COMPACT_REF) rather than a + * pointer: compactRefLoadLines decodes it, compactRefStoreLines encodes it. */ + compressed: boolean[] = []; constructor(readonly def: IrClassDef) { this.root = this; @@ -395,13 +399,24 @@ export function classEnvironmentIndex(meta: LlClassMeta): number { /** A field's GEP index inside its class struct: rc at 0, the vtable word * at 1 on hierarchy members, then the flattened field list. */ -export function classFieldIndex(meta: LlClassMeta, field: string): { index: number; type: IrType } { +export function classFieldIndex( + meta: LlClassMeta, + field: string, + refs: "pointer" | "compressed" = "pointer", +): { index: number; type: IrType; compressed: boolean } { const idx = meta.def.fields.findIndex((f) => f.name === field); if (idx < 0) throw new InternalCompilerError( `llvm emitter bug: unknown field ${field} on class ${meta.def.name}`, ); - return { index: classFieldSlot(meta, idx), type: meta.def.fields[idx]!.type }; + const compressed = meta.compressed[idx] === true; + // Callers that read or write a field's storage as a pointer must not see + // a compressed one; callers that decode and encode pass "compressed". + if (compressed && refs === "pointer") + throw new InternalCompilerError( + `llvm emitter bug: field ${field} of ${meta.def.name} holds a 32-bit reference`, + ); + return { index: classFieldSlot(meta, idx), type: meta.def.fields[idx]!.type, compressed }; } /** The GEP index of the field at `idx` in `meta.def.fields`. */ @@ -590,6 +605,75 @@ export function compactHeaderWord(meta: LlClassMeta): number { return COMPACT_HEADER_TAG | (meta.hierarchy ? meta.pre + 1 : 0); } +/** Lines that decode the 32-bit reference `idx` (an i32) of a compressed + * field into the pointer `out`. The program-lifetime region lies below + * 32 GiB (LlvmTargetOptions.lowRegion), so a reference is the instance's + * address over 8 and 0 is NULL: one shift, which x86-64 folds into the + * address of the next load. `t` names the temporaries. */ +export function compactRefLoadLines(idx: string, out: string, t: string): string[] { + return [ + ` ${t}.z = zext i32 ${idx} to i64`, + ` ${t}.o = shl nuw nsw i64 ${t}.z, 3`, + ` ${out} = inttoptr i64 ${t}.o to ptr`, + ]; +} + +/** Lines that encode the pointer `ptr` (NULL or a compact class instance) + * as the 32-bit reference `out` (an i32) a compressed field stores. */ +export function compactRefStoreLines(ptr: string, out: string, t: string): string[] { + return [ + ` ${t}.pi = ptrtoint ptr ${ptr} to i64`, + ` ${t}.s = lshr i64 ${t}.pi, 3`, + ` ${out} = trunc i64 ${t}.s to i32`, + ]; +} + +/** The storage type of a compressed field (LlClassMeta.compressed): a + * 32-bit index into the program-lifetime region. */ +export const COMPACT_REF = "i32"; + +/** Lines that allocate `%o`, `size` zeroed bytes in the program-lifetime + * region: a bump of the calling thread's chunk (scr_rc.h ScrPlrChunk, + * thread-local in worker programs), and scr_plr_alloc when the chunk is + * exhausted or the target has no inline allocation. The lines end in an + * open block. */ +function plrAllocLines(host: ClassHost, size: string): string[] { + const S = host.sizeType; + host.declare(`declare ptr @scr_plr_alloc(${S})`); + host.declare(`declare i1 @llvm.expect.i1(i1, i1)`); + if (host.inlineAlloc !== true) return [` %o = call ptr @scr_plr_alloc(${S} ${size})`]; + const chunk: string[] = []; + if (host.threadLocalAlloc === true) { + host.declare(`@scr_plr_chunk = external thread_local global { ptr, ptr }`); + host.declare(`declare nonnull ptr @llvm.threadlocal.address.p0(ptr nonnull)`); + chunk.push(` %plr.c = call ptr @llvm.threadlocal.address.p0(ptr @scr_plr_chunk)`); + } else { + host.declare(`@scr_plr_chunk = external dso_local global { ptr, ptr }`); + chunk.push(` %plr.c = getelementptr inbounds i8, ptr @scr_plr_chunk, i64 0`); + } + // A thread without a chunk has NULL for both, so nothing fits. + return [ + ...chunk, + ` %plr.lp = getelementptr inbounds i8, ptr %plr.c, i64 8`, + ` %plr.next = load ptr, ptr %plr.c`, + ` %plr.lim = load ptr, ptr %plr.lp`, + ` %plr.n7 = add ${S} ${size}, 7`, + ` %plr.n = and ${S} %plr.n7, -8`, + ` %plr.end = getelementptr i8, ptr %plr.next, ${S} %plr.n`, + ` %plr.fits = icmp ule ptr %plr.end, %plr.lim`, + ` %plr.fitsx = call i1 @llvm.expect.i1(i1 %plr.fits, i1 true)`, + ` br i1 %plr.fitsx, label %plr.bump, label %plr.slow`, + `plr.bump:`, + ` store ptr %plr.end, ptr %plr.c`, + ` br label %plr.done`, + `plr.slow:`, + ` %plr.o = call ptr @scr_plr_alloc(${S} ${size})`, + ` br label %plr.done`, + `plr.done:`, + ` %o = phi ptr [ %plr.next, %plr.bump ], [ %plr.o, %plr.slow ]`, + ]; +} + /** Byte size and alignment of a class field's LLVM storage type. */ function fieldStorage(type: string): { size: number; align: number } { switch (type) { @@ -628,7 +712,16 @@ export function assignCompactLayouts( metaMap: ReadonlyMap, compact: (root: LlClassMeta) => boolean, fieldTypes: (cls: IrClassDef) => string[], + compressedRef?: ( + cls: IrClassDef, + field: number, + isCompact: (className: string) => boolean, + ) => boolean, ): void { + const isCompact = (className: string): boolean => { + const target = metaMap.get(className); + return target !== undefined && target.def.runtime !== true && compact(target.root); + }; const byPre = [...metaMap.values()].sort((a, b) => a.pre - b.pre); for (const meta of byPre) { if (meta.def.runtime === true || !compact(meta.root)) continue; @@ -642,7 +735,22 @@ export function assignCompactLayouts( `llvm emitter bug: compact class ${meta.def.name} does not extend its base's layout`, ); const elems = inherited.map((e) => ({ ...e })); - const types = fieldTypes(meta.def); + // A reference to a compact class instance is a 32-bit index into the + // program-lifetime region (compactRefLoadLines); inherited fields keep the + // base's choice. + const compressed = meta.def.fields.map((_, i) => + i < baseCount + ? base!.compressed[i]! + : compressedRef !== undefined && compressedRef(meta.def, i, isCompact), + ); + const types = fieldTypes(meta.def).map((t, i) => { + if (!compressed[i]) return t; + if (t !== "ptr") + throw new InternalCompilerError( + `llvm emitter bug: compressed field ${meta.def.fields[i]!.name} of ${meta.def.name} is stored as ${t}`, + ); + return COMPACT_REF; + }); const own = types .map((type, field) => ({ type, field, ...fieldStorage(type) })) .slice(baseCount) @@ -679,6 +787,7 @@ export function assignCompactLayouts( meta.compact = true; meta.compactElems = elems; meta.compactSlots = slots; + meta.compressed = compressed; } } @@ -705,7 +814,10 @@ function undefFieldInits( if (f.name === "%cause") return; // A nullable-pointer field's zeroed slot already is its unit arm. if (nullable?.get(meta.def.name, f.name)) return; - const { index } = classFieldIndex(meta, f.name); + // Only union and dyn fields get a store here; a compressed field is a + // class reference, which starts NULL. + const { index, compressed } = classFieldIndex(meta, f.name, "compressed"); + if (compressed) return; if (f.type.kind === "union" && host.inlineUnions.classField(meta.def.name, f.name)) { // An inline field starts at the undefined arm's tag with zero bits. const tag = undefinedArmTag(f.type, host.unionsById); @@ -748,6 +860,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)); const typeDefs: string[] = []; const defs: string[] = []; const emitted = (mod.classes ?? []).filter((c) => !c.runtime); @@ -897,13 +1012,17 @@ export function emitClassShapes( const fieldIndex = (i: number): number => classFieldSlot(meta, i); // Teardown, trace and gcFree see each slot's storage type: a nullable // class field holds the instance pointer (NULL for the unit arm), and - // every class retain/release/visit entry point is NULL-tolerant. + // every class retain/release/visit entry point is NULL-tolerant. A + // compressed field refers to a compact instance, which nothing releases + // or traces, so they skip it. const indexedFields = [ - ...ownedFields(cls).map(({ field, i }) => ({ - name: field.name, - type: storedFieldType(cls, field), - index: fieldIndex(i), - })), + ...ownedFields(cls) + .filter(({ i }) => meta.compressed[i] !== true) + .map(({ field, i }) => ({ + name: field.name, + type: storedFieldType(cls, field), + index: fieldIndex(i), + })), ]; const refFields = indexedFields.filter((f) => isRefCounted(f.type)); const tracedFields = traced @@ -1118,6 +1237,11 @@ export function emitClassShapes( if (regions !== null) { nw.push(...regions.newAlloc(cls.name, sizeOf)); defs.push(...regions.classDefinitions(cls, sizeOf)); + } else if (meta.compact && regionRefs) { + // Compressed fields refer to compact instances by their index in the + // program-lifetime region, so every compact instance lives there: + // zeroed, 8-byte aligned and never freed. + nw.push(...plrAllocLines(host, sizeOf)); } else nw.push( ...emitObjectAlloc( diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 7bbd3d4538..0980925cbb 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -153,6 +153,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { : new Map(); emitter.integerViews.clear(); emitter.fieldPointerTags.clear(); + emitter.compressedFieldPtrs.clear(); emitter.integerArrayBindings.clear(); emitter.loopArrayBorrows = emitter.debug === null diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 5ccf7ccd8a..3d9d410720 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -545,6 +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()`] : []), `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()`] : []), @@ -837,6 +838,9 @@ export function emitModuleParts(emitter: LlEmitter): string[] { ...(emitter.runtimeAbiMarker ? [` call void @${RUNTIME_ABI_MARKER}()`] : []), ...(emitter.mod.workers ? [` call void @scr_runtime_workers_v8()`] : []), ` 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()`] : []), // 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 85f6c8dd7b..d1dc6369b8 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -558,7 +558,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.emitPublishedFieldGuard(obj.name, null, s.field); const { ptr, type, inline } = s.kind === "fieldSet" - ? emitter.classFieldPtr(obj.name, s.className, s.field) + ? emitter.classFieldPtr(obj.name, s.className, s.field, "compressed") : emitter.recordFieldPtr(obj.name, s.shapeId, s.field); // A result-slot entry fills zeroed caller storage, each field once. if ( diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 0bdbe23b2b..0badf40001 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -266,6 +266,8 @@ import { classFieldTypes, classMembershipIntervals, classFieldIndex, + compactRefLoadLines, + compactRefStoreLines, classPreLines, classStructSym, programLifetimeClass, @@ -423,6 +425,20 @@ export interface LlvmTargetOptions { /** Translated WebAssembly memories check bounds explicitly instead of * relying on guard pages (wasm-modules.ts wasmBoundsChecks). */ wasmBoundsChecks?: boolean; + /** The executable can reserve its program-lifetime region below 32 GiB + * (lowRegionTarget): fields that refer to compact class instances then + * store the instance's address over 8 in 32 bits (classes.ts + * compactRefLoadLines). Elsewhere they keep 8-byte pointers. */ + lowRegion?: boolean; +} + +/** LlvmTargetOptions.lowRegion for an executable: Linux targets without + * sanitizers, whose address space below 32 GiB is free (ASan keeps its + * shadow memory there; other systems map their own images and libraries + * there). `platform` is a target platform (`linux`, `darwin`, `win32`, + * `wasi`). */ +export function lowRegionTarget(platform: string, sanitize: boolean): boolean { + return platform === "linux" && !sanitize; } export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string { @@ -593,6 +609,13 @@ export class LlEmitter { integerArrayBindings = new Set(); readonly fieldAliasTags = new Map(); readonly fieldPointerTags = new Map(); + /** The field pointers (classFieldPtr) of this function whose fields hold + * 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; /** Native class fields with different property names cannot occupy the * same slot of one object. Inherited views of the same property share a @@ -1507,7 +1530,32 @@ export class LlEmitter { this.classMeta, (root) => programLifetimeClass(this.shapeHost, root) && !root.threadSlot, (cls) => classFieldTypes(this.shapeHost, cls, this.int32Slots), + // Executables whose region lies below 32 GiB hold compact class + // instances there (scr_plr_alloc), so fields referring to one store + // a 32-bit reference: the field's type, the class of a + // `C | undefined` or `C | null` pointer, or the reference arm of a + // nullable pointer field, is a compact class. `C | null | undefined` + // keeps the pointer: its null is a sentinel outside the region. + mod.lib === undefined && options.lowRegion === true + ? (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); + return nullable !== null && isCompact(nullable.arm.className); + } + : undefined, ); + this.usesCompactRefs = [...this.classMeta.values()].some((m) => m.compressed.some(Boolean)); 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 @@ -3217,15 +3265,47 @@ export class LlEmitter { objName: string, className: string, field: string, - ): { ptr: string; type: IrType; inline: boolean } { + refs: "pointer" | "compressed" = "pointer", + ): { ptr: string; type: IrType; inline: boolean; compressed: boolean } { const meta = this.classMetaOf(className); - const { index, type } = classFieldIndex(meta, field); + const { index, type, compressed } = classFieldIndex(meta, field, refs); const p = this.B.tmp(); this.B.line( `${p} = getelementptr inbounds %${classStructSym(className)}, ptr ${objName}, i64 0, i32 ${index}`, ); this.markFieldPointer(p, field); - return { ptr: p, type, inline: this.inlineUnions.classField(className, field) }; + if (compressed) this.compressedFieldPtrs.add(p); + return { ptr: p, type, inline: this.inlineUnions.classField(className, field), compressed }; + } + + /** 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. */ + loadRef(ptr: string, tagged = true): string { + const B = this.B; + const v = B.tmp(); + const tags = tagged ? this.fieldAliasAttachment(ptr) : ""; + if (!this.compressedFieldPtrs.has(ptr)) { + B.line(`${v} = load ptr, ptr ${ptr}${tags}`); + return v; + } + const idx = B.tmp(); + B.line(`${idx} = load i32, ptr ${ptr}${tags}`); + for (const line of compactRefLoadLines(idx, v, `${v}.r`)) B.line(line.trimStart()); + return v; + } + + /** Stores `value` (a pointer) through a field pointer: as is, or encoded + * as the 32-bit reference of a compressed field. */ + storeRef(ptr: string, value: string): void { + const B = this.B; + if (!this.compressedFieldPtrs.has(ptr)) { + B.line(`store ptr ${value}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`); + return; + } + const idx = B.tmp(); + for (const line of compactRefStoreLines(value, idx, `${idx}.r`)) B.line(line.trimStart()); + B.line(`store i32 ${idx}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`); } /** The dynamic class's preorder number (borrowed object; the static class @@ -3357,6 +3437,7 @@ export class LlEmitter { const { tag, bits } = loadInlineUnion(B, ptr); return stackUnionBox(B, this.sizeType, tag, bits); } + if (this.compressedFieldPtrs.has(ptr)) return this.loadRef(ptr); const fieldTy = llFieldType(t, false); const raw = B.tmp(); B.line(`${raw} = load ${fieldTy}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`); @@ -3383,8 +3464,7 @@ export class LlEmitter { replaceField(ptr: string, type: IrType, inline: boolean, v: LlValue): void { if (!inline && isRefCounted(type)) { this.toHeap(v); - const old = this.B.tmp(); - this.B.line(`${old} = load ptr, ptr ${ptr}`); + const old = this.loadRef(ptr, false); this.storeField(ptr, type, v.name, false); this.releaseHeap(old, type); return; @@ -3408,6 +3488,10 @@ export class LlEmitter { storeInlineUnion(B, ptr, tag, bits); return; } + if (this.compressedFieldPtrs.has(ptr)) { + this.storeRef(ptr, value); + return; + } const fieldTy = llFieldType(t, false); if (fieldTy !== "i8") { B.line(`store ${fieldTy} ${value}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`); @@ -5182,8 +5266,9 @@ export class LlEmitter { : [ { name: f.name, - index: classFieldIndex(meta, f.name).index, + index: classFieldIndex(meta, f.name, "compressed").index, type: this.nullableFields.storageType(className, f.name, f.type), + compressed: classFieldIndex(meta, f.name, "compressed").compressed, }, ], ), @@ -5390,11 +5475,9 @@ export class LlEmitter { emitUnionProjection(e: IrExpr): LlValue { const nullable = this.nullableFieldGet(e); if (!nullable || e.kind !== "fieldGet") return this.emitReadReceiver(e); - const B = this.B; const receiver = this.emitReadReceiver(e.obj); - const { ptr } = this.classFieldPtr(receiver.name, e.className, e.field); - const p = B.tmp(); - B.line(`${p} = load ptr, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`); + const { ptr } = this.classFieldPtr(receiver.name, e.className, e.field, "compressed"); + const p = this.loadRef(ptr); return { name: this.stackNullableBox(p, nullable), type: e.type }; } @@ -5407,9 +5490,8 @@ export class LlEmitter { if (!nullable || e.kind !== "fieldGet") throw new InternalCompilerError("llvm emitter bug: owned nullable stack of non-field"); const receiver = this.emitReadReceiver(e.obj); - const { ptr } = this.classFieldPtr(receiver.name, e.className, e.field); - const p = this.B.tmp(); - this.B.line(`${p} = load ptr, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`); + const { ptr } = this.classFieldPtr(receiver.name, e.className, e.field, "compressed"); + const p = this.loadRef(ptr); const owner = this.own({ name: this.retainFrame(p, nullable.arm), type: nullable.arm }); return { box: this.stackNullableBox(owner.name, nullable), owner }; } @@ -5486,7 +5568,6 @@ export class LlEmitter { nullable: NullableRefField, value: IrExpr, ): void { - const B = this.B; let next: string; if ( value.kind === "unionWrap" && @@ -5509,10 +5590,9 @@ export class LlEmitter { } else { next = this.unionToNullable(this.emitUnionProjection(value).name, nullable); } - const { ptr } = this.classFieldPtr(obj, className, field); - const old = B.tmp(); - B.line(`${old} = load ptr, ptr ${ptr}`); - B.line(`store ptr ${next}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`); + const { ptr } = this.classFieldPtr(obj, className, field, "compressed"); + const old = this.loadRef(ptr, false); + this.storeRef(ptr, next); this.releaseHeap(old, nullable.arm); } diff --git a/packages/compiler/src/backend/llvm/expr-control.ts b/packages/compiler/src/backend/llvm/expr-control.ts index fd2061aaed..242f9997cf 100644 --- a/packages/compiler/src/backend/llvm/expr-control.ts +++ b/packages/compiler/src/backend/llvm/expr-control.ts @@ -510,9 +510,13 @@ export function emitOptionalChainReceiver( // pointer itself, through a private stack box when the union's own // representation is boxed (emitUnionProjection's form). const owner = host.emitReadReceiver(e.receiver.obj); - const { ptr } = host.classFieldPtr(owner.name, e.receiver.className, e.receiver.field); - const p = B.tmp(); - B.line(`${p} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + const { ptr } = host.classFieldPtr( + owner.name, + e.receiver.className, + e.receiver.field, + "compressed", + ); + const p = host.loadRef(ptr); r = { name: host.nullableUnions.get(def.id) ? p : host.stackNullableBox(p, nullableField), type: e.receiver.type, diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 554306f6e0..563a900f8f 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -344,7 +344,7 @@ export function emitDynamicExpr( const payload = host.unionPeek(u.name, def.id); const { ptr, type, inline } = arm.kind === "object" - ? host.classFieldPtr(payload, arm.className, e.field) + ? host.classFieldPtr(payload, arm.className, e.field, "compressed") : host.recordFieldPtr(payload, arm.shapeId, e.field); const nullable = arm.kind === "object" ? host.nullableFields.get(arm.className, e.field) : null; diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index d6a33ceeb2..12e488b6db 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -1064,13 +1064,17 @@ export function emitRecordExpr( switch (e.kind) { case "fieldGet": { const obj = host.emitReadReceiver(e.obj); - const { ptr, type, inline } = host.classFieldPtr(obj.name, e.className, e.field); + const { ptr, type, inline } = host.classFieldPtr( + obj.name, + e.className, + e.field, + "compressed", + ); if (host.int32Slots.isField(e.className, e.field)) return host.loadInt32Field(ptr, e.type); if (inline) return host.own({ name: host.inlineFieldUnion(ptr, type), type: e.type }); const nullable = host.nullableFieldGet(e); if (nullable) { - const p = B.tmp(); - B.line(`${p} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`); + const p = host.loadRef(ptr); return host.own({ name: host.nullableToOwnedUnion(p, nullable), type: e.type }); } const v = host.loadField(ptr, type, false); diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index a4f2ed749e..a0a0cf24a6 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -20,7 +20,14 @@ import { } from "../../ir/ir.js"; import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js"; import { BlockBuilder } from "./blocks.js"; -import { type LlClassMeta, classFieldIndex, classPreLines, classStructSym } from "./classes.js"; +import { + type LlClassMeta, + classFieldIndex, + classPreLines, + classStructSym, + compactRefLoadLines, + compactRefStoreLines, +} from "./classes.js"; import { SCR_ARR_FIELD, llvmCommentText } from "./common.js"; import { FN_ATTRS, @@ -280,7 +287,7 @@ export function streamTypedRefCommitAdapter( const pending = `f${index}_pending`; const store = `f${index}_store`; const after = `f${index}_after`; - const { index: fieldIndex } = classFieldIndex(meta, field.name); + const { index: fieldIndex, compressed } = classFieldIndex(meta, field.name, "compressed"); const int32 = host.int32Slots.isField(t.className, field.name); const inline = host.inlineUnions.classField(t.className, field.name); const fieldTy = int32 ? "i32" : llFieldType(field.type, inline); @@ -335,7 +342,12 @@ export function streamTypedRefCommitAdapter( ` br i1 %${pending}, label %done, label %${store}`, `${store}:`, ` %f${index}_ptr = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 ${fieldIndex}`, - ` %f${index}_old = load ${fieldTy}, ptr %f${index}_ptr`, + ...(compressed + ? [ + ` %f${index}_oldidx = load i32, ptr %f${index}_ptr`, + ...compactRefLoadLines(`%f${index}_oldidx`, `%f${index}_old`, `%f${index}_od`), + ] + : [` %f${index}_old = load ${fieldTy}, ptr %f${index}_ptr`]), ); if (int32) { // int32-slots.ts keeps a field int32 only when no dynamic store in @@ -375,7 +387,12 @@ export function streamTypedRefCommitAdapter( ` %f${index}_pay = load ptr, ptr %f${index}_payp`, ` %f${index}_sel = select i1 %f${index}_isref, ptr %f${index}_pay, ptr null`, ` %f${index}_new = call ptr ${rc.retain}(ptr %f${index}_sel)`, - ` store ptr %f${index}_new, ptr %f${index}_ptr`, + ...(compressed + ? [ + ...compactRefStoreLines(`%f${index}_new`, `%f${index}_newidx`, `%f${index}_ne`), + ` store i32 %f${index}_newidx, ptr %f${index}_ptr`, + ] + : [` store ptr %f${index}_new, ptr %f${index}_ptr`]), ` call void @scr_union_release(ptr %${next})`, ` call void ${rc.release}(ptr %f${index}_old)`, ` br label %${after}`, @@ -398,6 +415,11 @@ export function streamTypedRefCommitAdapter( ` %f${index}_stored = zext i1 %${next} to i8`, ` store i8 %f${index}_stored, ptr %f${index}_ptr`, ); + } else if (compressed) { + lines.push( + ...compactRefStoreLines(`%${next}`, `%f${index}_nextidx`, `%f${index}_nx`), + ` store i32 %f${index}_nextidx, ptr %f${index}_ptr`, + ); } else { lines.push(` store ${fieldTy} %${next}, ptr %f${index}_ptr`); } @@ -832,7 +854,7 @@ export function streamTypedRefMaterializeAdapter( B.condBr(present, yes, afterField); B.startBlock(yes); } - const { index } = classFieldIndex(meta, field.name); + const { index, compressed } = classFieldIndex(meta, field.name, "compressed"); const fieldPtr = B.tmp(); let fieldValue = B.tmp(); B.line( @@ -844,7 +866,12 @@ export function streamTypedRefMaterializeAdapter( B.line(`${fieldValue} = sitofp i32 ${raw} to double`); } else if (host.inlineUnions.classField(t.className, field.name)) fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true); - else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`); + else if (compressed) { + const idx = B.tmp(); + B.line(`${idx} = load i32, ptr ${fieldPtr}`); + for (const line of compactRefLoadLines(idx, fieldValue, `${fieldValue}.r`)) + B.line(line.trimStart()); + } else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`); if (llFieldType(field.type, false) === "i8") { const boolValue = B.tmp(); B.line(`${boolValue} = trunc i8 ${fieldValue} to i1`); diff --git a/packages/compiler/src/backend/llvm/publish.ts b/packages/compiler/src/backend/llvm/publish.ts index bb7e0c8a86..2f45c3cd2b 100644 --- a/packages/compiler/src/backend/llvm/publish.ts +++ b/packages/compiler/src/backend/llvm/publish.ts @@ -25,7 +25,7 @@ import type { IrClassDef, IrRecordShape, IrType, IrUnionDef, SrcLoc } from "../. import { mapOf, STRING, typeKey } from "../../ir/ir.js"; import { mangleClassStruct, mangleRecordStruct } from "../mangle.js"; import { FN_ATTRS } from "./shapes.js"; -import { COMPACT_ID_MASK } from "./classes.js"; +import { COMPACT_ID_MASK, compactRefLoadLines } from "./classes.js"; import { shapeTagFn } from "./nullable-unions.js"; import type { InlineUnionFields } from "./inline-unions.js"; import { LlvmUnsupportedError } from "./unsupported.js"; @@ -69,8 +69,10 @@ export interface PublishClassInfo { idInRc: boolean; /** Instances have the 32-bit header (classes.ts LlClassMeta.compact). */ compact: boolean; - /** The class's own and inherited fields: GEP index and storage type. */ - fields: { name: string; index: number; type: IrType }[]; + /** The class's own and inherited fields: GEP index and storage type; + * `compressed` when the field holds a 32-bit reference (classes.ts + * LlClassMeta.compressed), which the walker decodes. */ + fields: { name: string; index: number; type: IrType; compressed?: boolean }[]; } /** scr_pub_mark's object kinds (SCR_PUB_KIND_* in scr_runtime.h), for the RC audit. */ @@ -536,7 +538,7 @@ export class PublishWalkers { head: string, struct: string, traced: boolean, - members: { name: string; index: number; type: IrType }[], + members: PublishClassInfo["fields"], path: string, loc: SrcLoc, classId = false, @@ -559,7 +561,12 @@ export class PublishWalkers { } lines.push( ` %f${n} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${member.index} ; ${llvmComment(member.name)}`, - ` %v${n} = load ptr, ptr %f${n}`, + ...(member.compressed === true + ? [ + ` %v${n}.i = load i32, ptr %f${n}`, + ...compactRefLoadLines(`%v${n}.i`, `%v${n}`, `%v${n}.d`), + ] + : [` %v${n} = load ptr, ptr %f${n}`]), ...this.push(`%v${n}`, fn, `${n}`, this.compactRef(member.type)), ); n++; diff --git a/packages/compiler/src/backend/llvm/receivers.ts b/packages/compiler/src/backend/llvm/receivers.ts index 50b9485a1b..67ca171881 100644 --- a/packages/compiler/src/backend/llvm/receivers.ts +++ b/packages/compiler/src/backend/llvm/receivers.ts @@ -118,7 +118,12 @@ export function emitReadReceiver(emitter: LlEmitter, e: IrExpr): LlValue { const { ptr, type, inline } = emitter.recordFieldPtr(receiver.name, e.shapeId, e.field); return { name: emitter.loadRecordField(ptr, type, inline), type: e.type }; } - const { ptr, type, inline } = emitter.classFieldPtr(receiver.name, e.className, e.field); + const { ptr, type, inline } = emitter.classFieldPtr( + receiver.name, + e.className, + e.field, + "compressed", + ); if (emitter.int32Slots.isField(e.className, e.field)) return emitter.loadInt32Field(ptr, e.type); return { name: emitter.loadField(ptr, type, inline), type: e.type }; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 92ee6b75eb..2b216c77de 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -67,7 +67,11 @@ import { mobileTargetRefusal, resolveCc, } from "./backend/external-c.js"; -import { emitLlvmModuleSource, LlvmUnsupportedError } from "./backend/llvm/emitter.js"; +import { + emitLlvmModuleSource, + LlvmUnsupportedError, + lowRegionTarget, +} from "./backend/llvm/emitter.js"; import { inlineRcOption } from "./backend/llvm/shapes.js"; import { emitNativeArtifact, @@ -782,6 +786,7 @@ async function prepareExecutableInput( wasi: buildPlatform === "wasi", runtimeAbiMarker: outputKind === "obj", objectAudit: opts.sanitize === true, + lowRegion: lowRegionTarget(buildPlatform, opts.sanitize === true), ...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}), ...inlineRcOption(opts.optimization, opts.sanitize === true), }); @@ -848,6 +853,7 @@ async function prepareExecutableInput( wasi: buildPlatform === "wasi", runtimeAbiMarker: useRuntimePack, objectAudit: opts.sanitize === true, + lowRegion: lowRegionTarget(buildPlatform, opts.sanitize === true), ...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}), ...inlineRcOption(opts.optimization, opts.sanitize === true), ...(opts.detachedExit === true ? { detachedExit: true } : {}), diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index b90760616c..6b0a2fea54 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 } from "../backend/llvm/emitter.js"; +import { emitLlvmModule, 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"; @@ -293,6 +293,7 @@ export function prepareNativeExecutable( wasi: toolchain.target.platform === "wasi", runtimeAbiMarker: outputKind === "obj" || outputKind === "exe", objectAudit: options.sanitize === true, + lowRegion: lowRegionTarget(toolchain.target.platform, options.sanitize === true), ...inlineRcOption(options.optimization, options.sanitize === true), ...(options.detachedExit === true && outputKind === "exe" ? { detachedExit: true } : {}), ...(options.optimization === "dev" && !options.strip diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4636-contract-program-lifetime-references.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4636-contract-program-lifetime-references.ts.json new file mode 100644 index 0000000000..a503a5aa06 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4636-contract-program-lifetime-references.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4636-contract-program-lifetime-references.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4637-contract-program-lifetime-workers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4637-contract-program-lifetime-workers.ts.json new file mode 100644 index 0000000000..c5d09c649a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4637-contract-program-lifetime-workers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4637-contract-program-lifetime-workers.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index 8cf14fb7f7..4b29b4b51e 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -1983,3 +1983,203 @@ void scr_heap_stats(double *total, double *used) { *total = (double)(held + sys_held); *used = (double)(held - (idle < held ? idle : held) + sys_used); } + +/* ── program-lifetime region ──────────────────────────────────────────── + * Instances of compact classes (llvm/classes.ts LlClassMeta.compact) in + * programs built for Linux without sanitizers live in one address range + * reserved for the whole process below 32 GiB, so a field that holds one + * can store its address over 8 in 32 bits (0 stays NULL). The instances + * are never freed. Each thread carves them from its own chunks of + * SCR_PLR_CHUNK bytes; a chunk is taken from the range with one atomic add. + * Pages are committed by first touch and start zeroed. Its strings are + * arrays rather than literals: programs without compact classes then link + * none of this, where literals would stay in the unit's merged strings. */ +#if defined(__linux__) && SIZE_MAX > UINT32_MAX +#include + +#define SCR_PLR_CHUNK ((size_t)1 << 20) +/* References are 32-bit addresses of 8-byte units, so the range ends by + * SCR_PLR_END. It starts at 4 GiB or above, clear of executables loaded low + * and their heaps; a refused size is halved down to SCR_PLR_MIN. */ +#define SCR_PLR_END ((size_t)32 << 30) +#define SCR_PLR_MIN ((size_t)1 << 30) +/* Past its first SCR_PLR_HUGE_AFTER bytes the range uses transparent huge + * pages, as the allocator's slices do (SCR_SA_HUGE_AFTER): a large heap of + * instances takes 512 times fewer page faults and TLB entries, and a small + * program stays on small pages. */ +#define SCR_PLR_HUGE_AFTER ((size_t)8 << 20) + +static char *scr_plr_base; /* the range's start, once reserved */ +static size_t scr_plr_size; +static size_t scr_plr_top; /* atomic */ +#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) +_Thread_local ScrPlrChunk scr_plr_chunk; +#else +ScrPlrChunk scr_plr_chunk; +#endif + +/* Reserves `size` bytes at `at` exactly; NULL if refused. */ +static char *scr_plr_map(void *at, size_t size) { + int flags = MAP_PRIVATE | MAP_ANON | MAP_NORESERVE; +#ifdef MAP_FIXED_NOREPLACE + flags |= MAP_FIXED_NOREPLACE; +#endif + char *p = mmap(at, size, PROT_READ | PROT_WRITE, flags, -1, 0); + if (p == MAP_FAILED) return NULL; + if (p != (char *)at) { + /* The system took `at` as a hint only (no MAP_FIXED_NOREPLACE). */ + (void)munmap(p, size); + return NULL; + } + return p; +} + +static __attribute__((noinline)) void scr_plr_reserve(void) { + static char lock; + while (__atomic_test_and_set(&lock, __ATOMIC_ACQUIRE)) { + } + if (!__atomic_load_n(&scr_plr_base, __ATOMIC_ACQUIRE)) { + char *p = NULL; + size_t size = 0; + for (size_t start = (size_t)4 << 30; !p && start <= (size_t)16 << 30; start *= 2) { + size = SCR_PLR_END - start; + while (size >= SCR_PLR_MIN && !(p = scr_plr_map((void *)start, size))) size /= 2; + } + if (p) { +#ifdef MADV_HUGEPAGE + (void)madvise(p + SCR_PLR_HUGE_AFTER, size - SCR_PLR_HUGE_AFTER, MADV_HUGEPAGE); +#endif + scr_plr_size = size; + /* The first chunk is never handed out, so every chunk is + * page-aligned. */ + __atomic_store_n(&scr_plr_top, SCR_PLR_CHUNK, __ATOMIC_RELAXED); + __atomic_store_n(&scr_plr_base, p, __ATOMIC_RELEASE); + } + } + __atomic_clear(&lock, __ATOMIC_RELEASE); + if (!scr_plr_base) { + static const char message[] = + "scriptc: cannot reserve address space below 32 GiB for program-lifetime objects\n"; + scr_trap(message); + } +} + +#ifdef SCR_SA_TRIM +/* A worker whose memory goes back when it exits (scr_sa_park_arena: it + * shared nothing) returns its chunks too, when the thread ends, and later + * chunks reuse them. A thread's chunks are linked through their first two + * words (the next chunk and the chunk's size) from scr_plr_mine; the first + * chunk a thread takes registers scr_plr_thread_end. */ +#define SCR_PLR_LINK 16 +static _Thread_local char *scr_plr_mine; +static pthread_key_t scr_plr_end_key; +static pthread_once_t scr_plr_end_once = PTHREAD_ONCE_INIT; +static pthread_mutex_t scr_plr_free_lock = PTHREAD_MUTEX_INITIALIZER; +static char **scr_plr_free; /* returned chunks of SCR_PLR_CHUNK bytes */ +static size_t scr_plr_nfree, scr_plr_free_cap; + +/* Thread exit, after scr_sa_thread_park decided whether the thread's memory + * goes back. SCR_SA_DISCARD=poison makes the chunks inaccessible instead. */ +static void scr_plr_thread_end(void *unused) { + (void)unused; + char *c = scr_plr_mine; + scr_plr_mine = NULL; + if (!scr_sa_discarded) return; + bool poison = scr_sa_discard_mode() == 2; + size_t returned = 0; + while (c) { + char *next = ((char **)c)[0]; + size_t size = ((size_t *)c)[1]; + returned += size; + if (poison) { + (void)mprotect(c, size, PROT_NONE); + } else { + (void)madvise(c, size, MADV_DONTNEED); /* reads as zero again */ + if (size == SCR_PLR_CHUNK) { + pthread_mutex_lock(&scr_plr_free_lock); + if (scr_plr_nfree == scr_plr_free_cap) { + size_t cap = scr_plr_free_cap ? 2 * scr_plr_free_cap : 64; + char **grown = realloc(scr_plr_free, cap * sizeof *grown); + if (grown) { + scr_plr_free = grown; + scr_plr_free_cap = cap; + } + } + if (scr_plr_nfree < scr_plr_free_cap) scr_plr_free[scr_plr_nfree++] = c; + pthread_mutex_unlock(&scr_plr_free_lock); + } + } + c = next; + } + static const char stats_line[] = "scr_sa: thread returned %zu MB of program-lifetime objects\n"; + const char *stats = getenv("SCR_SA_STATS"); + if (stats && stats[0] == '1') fprintf(stderr, stats_line, returned >> 20); +} + +static void scr_plr_end_key_init(void) { + (void)pthread_key_create(&scr_plr_end_key, scr_plr_thread_end); +} + +/* Records the calling thread's new chunk `p` of `size` bytes. */ +static void scr_plr_own(char *p, size_t size) { + if (!scr_plr_mine) { + (void)pthread_once(&scr_plr_end_once, scr_plr_end_key_init); + (void)pthread_setspecific(scr_plr_end_key, p); + } + ((char **)p)[0] = scr_plr_mine; + ((size_t *)p)[1] = size; + scr_plr_mine = p; +} + +static char *scr_plr_reuse(void) { + char *c = NULL; + pthread_mutex_lock(&scr_plr_free_lock); + if (scr_plr_nfree) c = scr_plr_free[--scr_plr_nfree]; + pthread_mutex_unlock(&scr_plr_free_lock); + return c; +} +#else +#define SCR_PLR_LINK 0 +#endif + +static __attribute__((noinline)) void *scr_plr_refill(size_t n) { + if (!__atomic_load_n(&scr_plr_base, __ATOMIC_ACQUIRE)) scr_plr_reserve(); + size_t need = n + SCR_PLR_LINK; + size_t chunk = + need > SCR_PLR_CHUNK ? (need + SCR_PLR_CHUNK - 1) & ~(SCR_PLR_CHUNK - 1) : SCR_PLR_CHUNK; + char *p = NULL; +#ifdef SCR_SA_TRIM + if (chunk == SCR_PLR_CHUNK) p = scr_plr_reuse(); +#endif + if (!p) { + size_t at = __atomic_fetch_add(&scr_plr_top, chunk, __ATOMIC_RELAXED); + if (at + chunk > scr_plr_size || at + chunk < at) { + static const char message[] = "scriptc: out of address space for program-lifetime objects\n"; + scr_trap(message); + } + p = scr_plr_base + at; + } +#ifdef SCR_SA_TRIM + scr_plr_own(p, chunk); +#endif + scr_plr_chunk.next = p + SCR_PLR_LINK + n; + scr_plr_chunk.limit = p + chunk; + return p + SCR_PLR_LINK; +} + +/* Called before compiled code runs (emitted main), so the region exists + * before any reference into it does. */ +void scr_plr_init(void) { + if (!__atomic_load_n(&scr_plr_base, __ATOMIC_ACQUIRE)) scr_plr_reserve(); +} + +void *scr_plr_alloc(size_t n) { + n = (n + 7) & ~(size_t)7; + char *p = scr_plr_chunk.next; + if (p && n <= (size_t)(scr_plr_chunk.limit - p)) { + scr_plr_chunk.next = p + n; + return p; + } + return scr_plr_refill(n); +} +#endif diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 892531965a..93fdd7946e 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -308,6 +308,30 @@ void scr_cyc_free(void *obj); /* frees the block, header included */ void *scr_mem_realloc(void *p, size_t n); void *scr_rt_calloc(size_t n); /* emitted-code entry points */ +#if defined(__linux__) && SIZE_MAX > UINT32_MAX +/* The program-lifetime region (scr_alloc.c): compact class instances of + * programs built for Linux without sanitizers (llvm/emitter.ts + * lowRegionTarget), which are never freed, at 8-byte aligned addresses below + * 32 GiB, so their fields can hold address >> 3 in 32 bits (0 is NULL). + * scr_plr_init reserves it before compiled code runs; scr_plr_alloc returns + * zeroed memory. */ +void scr_plr_init(void); +void *scr_plr_alloc(size_t n); +/* The calling script thread's part of the region: emitted allocation fast + * paths take [next, limit) in 8-byte multiples and call scr_plr_alloc when + * it is exhausted (llvm/classes.ts addresses both fields). */ +typedef struct ScrPlrChunk { + char *next; + char *limit; +} ScrPlrChunk; +#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) +extern _Thread_local ScrPlrChunk scr_plr_chunk; +#else +extern ScrPlrChunk scr_plr_chunk; +#endif +_Static_assert(offsetof(ScrPlrChunk, next) == 0 && offsetof(ScrPlrChunk, limit) == 8, + "llvm/classes.ts addresses ScrPlrChunk fields at these offsets"); +#endif void scr_rt_free(void *p); /* Asks the allocator's pre-fault helper (scr_alloc.c) to fault the pages * of [lo, hi), aligned to 256 KiB, in ahead of use; builds without the diff --git a/scripts/ci-test-costs.json b/scripts/ci-test-costs.json index 949e80dc85..4c49645796 100644 --- a/scripts/ci-test-costs.json +++ b/scripts/ci-test-costs.json @@ -364,6 +364,7 @@ "tests/harness/oci-manifest.test.ts": 3, "tests/harness/opaque-class-view.test.ts": 1743, "tests/harness/process-argv.test.ts": 1923, + "tests/harness/program-lifetime-workers.test.ts": 4200, "tests/harness/project-config.test.ts": 71955, "tests/harness/provenance.test.ts": 3971, "tests/harness/regex.test.ts": 4073, @@ -781,6 +782,7 @@ "tests/harness/oci-manifest.test.ts": 3, "tests/harness/opaque-class-view.test.ts": 2545, "tests/harness/process-argv.test.ts": 2583, + "tests/harness/program-lifetime-workers.test.ts": 5, "tests/harness/project-config.test.ts": 162430, "tests/harness/provenance.test.ts": 3734, "tests/harness/publish-leaks.test.ts": 29487, diff --git a/tests/corpus/4636-contract-program-lifetime-references.ts b/tests/corpus/4636-contract-program-lifetime-references.ts new file mode 100644 index 0000000000..1639f640ed --- /dev/null +++ b/tests/corpus/4636-contract-program-lifetime-references.ts @@ -0,0 +1,195 @@ +import type { ProgramLifetime } from "@scriptc/types"; + +// ProgramLifetime classes that refer to each other through fields: plain, +// optional (undefined) and nullable (null) references, chains and cycles, +// references inherited from a base class and added by subclasses, beside +// fields of every other kind. Reads, writes, compound and logical +// assignment, optional chaining, identity, Map and Set keys, closures, +// inspection, JSON and unknown values see the same objects. + +abstract class Item implements ProgramLifetime { + readonly id: number; + next: Item | undefined = undefined; + owner: Group | null = null; + label: string; + constructor(id: number, label: string) { + this.id = id; + this.label = label; + } + abstract describe(): string; +} + +class Leaf extends Item { + weight = 1; + twin: Leaf | undefined = undefined; + describe(): string { + return `Leaf(${this.id}:${this.label}, w=${this.weight}, twin=${this.twin?.id ?? "-"})`; + } +} + +class Group extends Item { + first: Item | undefined = undefined; + last: Item | undefined = undefined; + readonly root: Root; + count = 0; + flagged = false; + constructor(id: number, label: string, root: Root) { + super(id, label); + this.root = root; + } + add(item: Item): void { + item.owner = this; + if (this.last === undefined) this.first = item; + else this.last.next = item; + this.last = item; + this.count++; + } + describe(): string { + const parts: string[] = []; + for (let at = this.first; at !== undefined; at = at.next) parts.push(at.describe()); + return `Group(${this.id}:${this.label} [${parts.join(", ")}])`; + } +} + +class Root implements ProgramLifetime { + readonly name: string; + top: Group | undefined = undefined; + self: Root; + readonly groups: Group[] = []; + readonly byLabel = new Map(); + constructor(name: string) { + this.name = name; + this.self = this; + } +} + +class Pair implements ProgramLifetime { + left: Pair | undefined; + right: Pair | null; + readonly tag: string; + constructor(left: Pair | undefined, right: Pair | null, tag: string) { + this.left = left; + this.right = right; + this.tag = tag; + } +} + +// A mortal class holding program-lifetime references. +class Cursor { + at: Item | undefined; + steps = 0; + constructor(at: Item | undefined) { + this.at = at; + } + advance(): boolean { + if (this.at === undefined) return false; + this.at = this.at.next; + this.steps++; + return this.at !== undefined; + } +} + +const root = new Root("root"); +let id = 0; +for (let g = 0; g < 3; g++) { + const group = new Group(id++, `g${g}`, root); + root.groups.push(group); + root.byLabel.set(group.label, group); + for (let i = 0; i < 3 + g; i++) { + const leaf = new Leaf(id++, `l${g}.${i}`); + leaf.weight = g * 10 + i; + group.add(leaf); + root.byLabel.set(leaf.label, leaf); + } +} +root.top = root.groups[0]; +root.groups[0]!.add(root.groups[1]!); +root.groups[1]!.add(root.groups[2]!); + +console.log(root.top?.describe()); +console.log(root.self === root, root.self.self.name, root.top?.root === root); + +// Reads through chains and the owner back references. +const deep = root.byLabel.get("l2.4"); +console.log(deep?.owner?.owner?.owner?.label, deep?.owner?.root.name, deep?.next?.id ?? "end"); +let hops = 0; +for (let at: Item | null = deep ?? null; at !== null; at = at.owner) hops++; +console.log("hops", hops); + +// Writes: replace, clear and relink. +const g1 = root.groups[1]!; +const firstOfG1 = g1.first!; +g1.first = firstOfG1.next; +firstOfG1.next = undefined; +firstOfG1.owner = null; +console.log(g1.describe()); +console.log(firstOfG1.owner === null, firstOfG1.next === undefined, firstOfG1.describe()); + +// Logical and conditional assignment. +const twins: Leaf[] = []; +for (const label of ["l0.0", "l0.1", "l0.2"]) { + const leaf = root.byLabel.get(label); + if (leaf instanceof Leaf) twins.push(leaf); +} +twins[0]!.twin ??= twins[1]; +twins[0]!.twin ??= twins[2]; +twins[1]!.twin ||= twins[0]; +twins[2]!.twin = twins[2]!.twin ?? twins[2]; +twins[2]!.twin &&= twins[1]; +console.log(twins.map((t) => t.describe()).join(" | ")); + +// Identity: Map and Set keys, ===, and arrays of references. +const seen = new Set(); +const visits = new Map(); +for (let round = 0; round < 3; round++) { + for (let at = root.groups[0]!.first; at !== undefined; at = at.next) { + seen.add(at); + visits.set(at, (visits.get(at) ?? 0) + 1); + } +} +console.log(seen.size, [...visits.values()].join(","), seen.has(root.groups[1]!), seen.has(firstOfG1)); +console.log(root.groups.indexOf(g1), root.groups.some((g) => g === firstOfG1)); + +// A mortal cursor and closures that capture program-lifetime objects. +const cursor = new Cursor(root.groups[2]!.first); +while (cursor.advance()) {} +console.log("steps", cursor.steps, cursor.at === undefined); +const readers: (() => string)[] = []; +for (const g of root.groups) readers.push(() => g.last?.label ?? "none"); +root.groups[2]!.last = root.groups[2]!.first; +console.log(readers.map((r) => r()).join(",")); + +// A cycle through null and undefined arms. +const a = new Pair(undefined, null, "a"); +const b = new Pair(a, null, "b"); +a.right = b; +a.left = a; +const c = new Pair(b, a, "c"); +console.log(c.left?.left?.right?.tag, c.right?.left?.tag, b.right, a.left === a); +const pairs = [a, b, c]; +pairs.sort((x, y) => (x.right?.tag ?? "").localeCompare(y.right?.tag ?? "")); +console.log(pairs.map((p) => `${p.tag}<${p.left?.tag ?? "u"},${p.right?.tag ?? "n"}>`).join(" ")); + +// Inspection, JSON and references boxed into unknown. +const solo = new Leaf(100, "solo"); +const other = new Leaf(101, "other"); +solo.twin = other; +console.log(solo); +console.log(JSON.stringify(other)); +console.log(JSON.stringify(new Pair(new Pair(undefined, null, "inner"), null, "outer"))); +console.log(Object.keys(solo).join(",")); +const boxed: unknown[] = [solo, solo.twin, solo.next, solo.owner]; +console.log(boxed.map((v) => typeof v).join(","), boxed[0] === solo, boxed[1] === other); +console.log(boxed.indexOf(other), boxed.includes(undefined), boxed.includes(null)); + +// Many objects with references to their neighbours. +let chain: Leaf | undefined = undefined; +let total = 0; +for (let i = 0; i < 20000; i++) { + const leaf: Leaf = new Leaf(i, "n"); + leaf.twin = chain; + leaf.weight = i % 7; + chain = leaf; +} +for (let at = chain; at !== undefined; at = at.twin) total += at.weight; +console.log("chain", total, chain?.twin?.twin?.id); diff --git a/tests/corpus/4637-contract-program-lifetime-workers.ts b/tests/corpus/4637-contract-program-lifetime-workers.ts new file mode 100644 index 0000000000..bccdc240fc --- /dev/null +++ b/tests/corpus/4637-contract-program-lifetime-workers.ts @@ -0,0 +1,79 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; +import type { ProgramLifetime } from "@scriptc/types"; + +// Waves of workers that each build a graph of ProgramLifetime instances +// referring to each other, post a checksum of it and exit without sharing +// anything. scriptc never frees such instances while the worker runs; on +// Linux their memory goes back after the worker exits, like the rest of +// its memory, and later workers reuse it. + +class Item implements ProgramLifetime { + readonly id: number; + next: Item | undefined = undefined; + owner: Group | undefined = undefined; + constructor(id: number) { + this.id = id; + } +} + +class Group implements ProgramLifetime { + readonly name: string; + first: Item | undefined = undefined; + previous: Group | undefined = undefined; + count = 0; + constructor(name: string) { + this.name = name; + } + add(item: Item): void { + item.owner = this; + item.next = this.first; + this.first = item; + this.count++; + } +} + +function build(seed: number, groups: number, items: number): number { + let last: Group | undefined = undefined; + for (let g = 0; g < groups; g++) { + const group: Group = new Group(`g${seed}.${g}`); + group.previous = last; + for (let i = 0; i < items; i++) group.add(new Item(seed * 1000003 + g * items + i)); + last = group; + } + let sum = 0; + for (let group = last; group !== undefined; group = group.previous) { + for (let item = group.first; item !== undefined; item = item.next) { + if (item.owner === group) sum = (sum + item.id * (group.count % 97)) % 1000000007; + } + } + return sum; +} + +function runWorker(seed: number): Promise { + return new Promise((resolve, reject) => { + const worker = new Worker(new URL(import.meta.url), { workerData: seed }); + let sum = -1; + worker.on("message", (value: number) => { + sum = value; + }); + worker.on("error", reject); + worker.on("exit", () => resolve(sum)); + }); +} + +async function main(): Promise { + for (let wave = 0; wave < 3; wave++) { + const first = runWorker(wave * 2); + const second = runWorker(wave * 2 + 1); + console.log(`wave ${wave}: ${await first} ${await second}`); + } + // The workers' memory goes back shortly after the last one exits. + await new Promise((resolve) => setTimeout(resolve, 300)); + console.log("main", build(99, 4, 1000)); +} + +if (isMainThread) { + void main(); +} else { + parentPort!.postMessage(build(workerData as number, 50, 4000)); +} diff --git a/tests/harness/corpus-costs/4636-contract-program-lifetime-references.ts.json b/tests/harness/corpus-costs/4636-contract-program-lifetime-references.ts.json new file mode 100644 index 0000000000..93afb36c0b --- /dev/null +++ b/tests/harness/corpus-costs/4636-contract-program-lifetime-references.ts.json @@ -0,0 +1 @@ +{ "plain": 4300, "san": 7900 } diff --git a/tests/harness/corpus-costs/4637-contract-program-lifetime-workers.ts.json b/tests/harness/corpus-costs/4637-contract-program-lifetime-workers.ts.json new file mode 100644 index 0000000000..35b1a9df37 --- /dev/null +++ b/tests/harness/corpus-costs/4637-contract-program-lifetime-workers.ts.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 4000 } diff --git a/tests/harness/program-lifetime-workers.test.ts b/tests/harness/program-lifetime-workers.test.ts new file mode 100644 index 0000000000..8172fbcde9 --- /dev/null +++ b/tests/harness/program-lifetime-workers.test.ts @@ -0,0 +1,48 @@ +/* ProgramLifetime instances that a worker creates are never freed while it + * runs, but on Linux they go back with the rest of its memory when it exits + * without sharing anything. The corpus program pins Node.js parity; this + * suite runs it with the allocator's statistics (SCR_SA_STATS=1) and checks + * that each discarded worker returned its program-lifetime objects. Under + * SCRIPTC_SAN=1 the runtime keeps every worker's memory, so the check does + * not apply. */ +import { execFile } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const exec = promisify(execFile); +const program = join(import.meta.dirname, "../corpus/4637-contract-program-lifetime-workers.ts"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +test.skipIf(process.platform !== "linux" || sanitize)( + "workers that exit without sharing return their ProgramLifetime instances", + { timeout: 240_000 }, + async () => { + const outDir = await mkdtemp(join(tmpdir(), "scriptc-program-lifetime-workers-")); + try { + const result = await compile(program, { + backend: "llvm", + outDir, + outPath: join(outDir, "program"), + }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + if (!result.ok) return; + const { stdout, stderr } = await exec(result.binaryPath, [], { + env: { ...process.env, SCR_SA_STATS: "1", SCR_SA_DISCARD_DELAY_MS: "0" }, + timeout: 120_000, + }); + expect(stdout).toContain("wave 2: "); + // One line per discarded worker; each built 200,000 items in 50 groups. + const returned = [...stderr.matchAll(/(\d+) MB of program-lifetime objects/g)].map((m) => + Number(m[1]), + ); + expect(returned.length).toBeGreaterThan(0); + for (const mb of returned) expect(mb).toBeGreaterThanOrEqual(4); + } finally { + await rm(outDir, { recursive: true, force: true }); + } + }, +); From 5bdc3be385c8c6e222a229ce9735ab372f8842cf Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 23:25:48 -0500 Subject: [PATCH 051/395] Name function expressions by their source names in SC6101 and skip type names and generated helpers --- docs/content/docs/coverage.mdx | 2 +- .../src/frontend/lowering/dynamic-tier.ts | 57 ++++++++++++++++--- .../dynamic-tier/function-names.js | 32 +++++++++++ .../dynamic-tier/typed-hot.ts | 22 ++++++- .../coverage-fixtures/dynamic-tier/unnamed.ts | 41 +++++++++++++ tests/harness/coverage.test.ts | 24 +++++++- 6 files changed, 162 insertions(+), 16 deletions(-) create mode 100644 tests/coverage-fixtures/dynamic-tier/function-names.js create mode 100644 tests/coverage-fixtures/dynamic-tier/unnamed.ts diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index d2fc345ad9..ae36f736d1 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -79,7 +79,7 @@ The program compiled. Adding types to these functions lets scriptc compile them Without a profile, a dynamic-tier function counts as likely to run often when a dynamic operation runs on every iteration of one of its loops (a `for` loop's initializer and a `for...of` loop's iterable run once), when a loop calls it, or when it calls itself. A function is named only when its source says why its values are untyped: a value typed `any` (for a `for...of` variable, the value it iterates), an untyped `JSON.parse` result, or an untyped JavaScript file. Dynamic code that runs once, such as setup code that reads an `any` configuration, is not reported, and neither are functions that are dynamic for an operation with no static lowering, which more types would not change. -The warning names at most five functions, most likely to run often first, and gives the total. Dynamic-tier code in npm packages is summarized on one line with each package's function count. A build that comes from the cache repeats the warning. `--no-dynamic-tier-warning` leaves it out, and `--fail-on=dynamic` fails a coverage run on it. +The warning names at most five functions, most likely to run often first, and gives the total. A function expression is named by its own name, else by the variable, property or class field it is assigned to, else as `anonymous function`. Dynamic-tier code in npm packages is summarized on one line with each package's function count. A build that comes from the cache repeats the warning. `--no-dynamic-tier-warning` leaves it out, and `--fail-on=dynamic` fails a coverage run on it. ## Diagnostic codes diff --git a/packages/compiler/src/frontend/lowering/dynamic-tier.ts b/packages/compiler/src/frontend/lowering/dynamic-tier.ts index 72cba99429..0293f9ab7c 100644 --- a/packages/compiler/src/frontend/lowering/dynamic-tier.ts +++ b/packages/compiler/src/frontend/lowering/dynamic-tier.ts @@ -172,10 +172,20 @@ function isJsonParse(node: ts.Node | undefined): boolean { ); } +/** Bindings that hold a value: variables, parameters and properties (not + * types, classes or functions, whose names an operand may also mention). */ +const VALUE_BINDING = + ts.SymbolFlags.FunctionScopedVariable | + ts.SymbolFlags.BlockScopedVariable | + ts.SymbolFlags.Property; + /** Why an identifier holds a value without a static type, or null. */ function identifierCause(lowerer: Lowerer, id: ts.Identifier, depth = 0): UntypedCause | null { - const symbol = lowerer.checker.getSymbolAtLocation(id); - const decl = symbol?.declarations?.[0]; + let symbol = lowerer.checker.getSymbolAtLocation(id); + if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) + symbol = lowerer.checker.getAliasedSymbol(symbol); + if (symbol === undefined || (symbol.flags & VALUE_BINDING) === 0) return null; + const decl = symbol.declarations?.[0]; // An unannotated `for (const item of items)` variable: name `items`. if ( depth < 4 && @@ -241,6 +251,9 @@ function causeOf(lowerer: Lowerer, sites: readonly { loc: SrcLoc }[]): UntypedCa return null; } +/** Generated functions (see scanDynamicTier), after any module prefix. */ +const GENERATED = /^%*(?:m\d+\.)?%*(?:dyn|caught|init)\./; + /** The likely-hot dynamic-tier functions of `module` (see the header). */ export function scanDynamicTier(lowerer: Lowerer, module: IrModule): DynamicTierReport { const facts = new Map(); @@ -255,9 +268,9 @@ export function scanDynamicTier(lowerer: Lowerer, module: IrModule): DynamicTier const packages = new Map(); for (const fn of module.functions) { // Generated `%dyn.*` helpers are dynamic operations lowered out of line, - // and `%init.*` is a module's top level: neither is a function to name. - if (fn.tier !== "dynamic" || fn.name.startsWith("%dyn.") || fn.name.startsWith("%init.")) - continue; + // `%caught.*` helpers test caught values, and `%init.*` is a module's top + // level: none is a function to name. + if (fn.tier !== "dynamic" || GENERATED.test(fn.name)) continue; if (isNodeModulesPath(fn.loc.file)) { const name = packageOf(fn.loc.file); packages.set(name, (packages.get(name) ?? 0) + 1); @@ -300,9 +313,35 @@ export function scanDynamicTier(lowerer: Lowerer, module: IrModule): DynamicTier }; } -/** `%%m144.Session.finish` → `Session.finish`, `m163.find` → `find`. */ -function displayName(name: string): string { - return name.replace(/^%+/, "").replace(/^m\d+\./, ""); +/** The name the source binds a function expression to (`totals` in + * `const totals = (rows) => ...`), or null. */ +function boundName(lowerer: Lowerer, loc: SrcLoc): string | null { + const sf = lowerer.program.getSourceFile(loc.file); + if (sf === undefined) return null; + const parent = nodeAt(sf, loc.start, loc.end).parent; + if ( + parent !== undefined && + (ts.isVariableDeclaration(parent) || + ts.isPropertyAssignment(parent) || + ts.isPropertyDeclaration(parent)) && + ts.isIdentifier(parent.name) + ) + return parent.name.text; + return null; +} + +/** A function's name as the source spells it, without a module prefix + * (`%%m144.Session.finish` → `Session.finish`, `m163.find` → `find`). A + * function expression (`%fn`) takes its own name, else the name it is + * bound to, else `anonymous function`. */ +function displayName(lowerer: Lowerer, f: HotDynamicFunction): string { + const plain = f.name + .replace(/^%+/, "") + .replace(/^m\d+\./, "") + .replace(/^%+/, ""); + const lambda = /^fn\d+(?:_(.+))?$/.exec(plain); + if (lambda === null) return plain; + return lambda[1] ?? boundName(lowerer, f.loc) ?? "anonymous function"; } /** SC6101 for the program lowering produced, or null (see the header). @@ -322,7 +361,7 @@ export function dynamicTierWarning( }; return dynamicTierDiag( report.hot.map((f) => ({ - name: displayName(f.name), + name: displayName(lowerer, f), where: where(f.loc), cause: f.cause, hot: f.hot, diff --git a/tests/coverage-fixtures/dynamic-tier/function-names.js b/tests/coverage-fixtures/dynamic-tier/function-names.js new file mode 100644 index 0000000000..3d6ab68075 --- /dev/null +++ b/tests/coverage-fixtures/dynamic-tier/function-names.js @@ -0,0 +1,32 @@ +// Function expressions in an untyped JavaScript file: SC6101 names each one +// by its own name, else by the class field, property or variable it is +// assigned to, else as an anonymous function. +class Totals { + sum = (rows) => { + let s = 0; + for (let i = 0; i < rows.length; i++) s += rows[i].n; + return s; + }; +} + +const ops = { + count: (rows) => { + let c = 0; + for (let i = 0; i < rows.length; i++) if (rows[i].n > 1) c++; + return c; + }, +}; + +const named = function scale(rows) { + let s = 0; + for (let i = 0; i < rows.length; i++) s += rows[i].n * 2; + return s; +}; + +const rows = [{ n: 1 }, { n: 2 }]; +const results = [rows].map((list) => { + let s = 0; + for (let i = 0; i < list.length; i++) s += list[i].n; + return s; +}); +console.log(new Totals().sum(rows), ops.count(rows), named(rows), results[0]); diff --git a/tests/coverage-fixtures/dynamic-tier/typed-hot.ts b/tests/coverage-fixtures/dynamic-tier/typed-hot.ts index e72a7ca7ad..804170238f 100644 --- a/tests/coverage-fixtures/dynamic-tier/typed-hot.ts +++ b/tests/coverage-fixtures/dynamic-tier/typed-hot.ts @@ -1,5 +1,7 @@ -// TypeScript functions that loop over untyped values: an `any` parameter -// and an untyped JSON.parse result (SC6101 names both). +// TypeScript functions that loop over untyped values: an `any` parameter, +// an untyped JSON.parse result and an arrow function's `any` rows (SC6101 +// names all three, the arrow by the name it is bound to). A cast to an +// alias of `any` blames the untyped value it produces, not the alias. function regionTotals(items: any): Map { const out = new Map(); for (const item of items) out.set(item.region, (out.get(item.region) ?? 0) + item.amount); @@ -13,9 +15,23 @@ function countRegions(text: string): number { return n; } +const weights = (rows: any): number => { + let sum = 0; + for (let i = 0; i < rows.length; i++) sum += rows[i].amount; + return sum; +}; + +type Loose = any; + +function entryCount(value: unknown): number { + let n = 0; + for (const key of value as Loose) if (key !== "") n++; + return n; +} + const rows = [ { amount: 3, region: "eu" }, { amount: 4, region: "us" }, ]; for (const [region, total] of regionTotals(rows)) console.log(region, total); -console.log(countRegions(JSON.stringify(rows))); +console.log(countRegions(JSON.stringify(rows)), weights(rows), entryCount(["a", ""])); diff --git a/tests/coverage-fixtures/dynamic-tier/unnamed.ts b/tests/coverage-fixtures/dynamic-tier/unnamed.ts new file mode 100644 index 0000000000..bdc53d81f3 --- /dev/null +++ b/tests/coverage-fixtures/dynamic-tier/unnamed.ts @@ -0,0 +1,41 @@ +// Dynamic loops that SC6101 does not name. `readEntries` loops over +// `unknown` values: the type name `Record` in its cast is not an untyped +// value. `readAll` catches into `Error` while a subclass of `Error` crosses +// into an `any` value, so its catch clause calls a generated check helper, +// which is not a source function. +function parse(text: string): Record | null { + const value: unknown = JSON.parse(text); + return value !== null && typeof value === "object" ? (value as Record) : null; +} + +function readEntries(text: string): Map { + const out = new Map(); + const entries = parse(text)?.["entries"]; + if (entries === null || typeof entries !== "object") return out; + for (const [name, raw] of Object.entries(entries as Record)) { + if (raw === null || typeof raw !== "object") continue; + const e = raw as { dir?: unknown; label?: unknown }; + if (typeof e.dir !== "string") continue; + out.set(name, typeof e.label === "string" ? `${e.dir}:${e.label}` : e.dir); + } + return out; +} + +class ReadError extends Error {} + +function readAll(texts: string[]): string[] { + const out: string[] = []; + for (const text of texts) { + try { + if (text === "") throw new ReadError("empty"); + out.push(text); + } catch (e) { + out.push(`cannot read: ${(e as Error).message}`); + } + } + return out; +} + +const box: any = { error: new ReadError("boxed") }; +console.log(readEntries('{"entries": {"a": {"dir": "x", "label": "y"}}}').size); +console.log(readAll(["one", ""]).join(", "), box.error.message); diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index ca62352b11..57ac7187cf 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -133,9 +133,11 @@ test("likely-hot functions with untyped values report as a performance warning", test("the performance warning names the untyped value of each function", () => { const { coverage } = analyze(fixture("dynamic-tier/typed-hot.ts")); expect(coverage.performance?.[0]?.message.split("\n")).toEqual([ - "2 functions that likely run often use untyped values, which runs slower than typed code:", - " regionTotals (typed-hot.ts:3): `items` is `any`, in a loop", - " countRegions (typed-hot.ts:9): `parsed` holds an untyped `JSON.parse` result, in a loop", + "4 functions that likely run often use untyped values, which runs slower than typed code:", + " regionTotals (typed-hot.ts:5): `items` is `any`, in a loop", + " entryCount (typed-hot.ts:26): `key` is `any`, in a loop", + " countRegions (typed-hot.ts:11): `parsed` holds an untyped `JSON.parse` result, in a loop", + " weights (typed-hot.ts:18): `rows` is `any`, in a loop", ]); }); @@ -146,6 +148,22 @@ test("untyped setup code that runs once reports no performance warning", () => { expect(coveragePasses(coverage, "dynamic")).toBe(true); }); +test("the performance warning names function expressions as the source does", () => { + const { coverage } = analyze(fixture("dynamic-tier/function-names.js")); + expect(coverage.performance?.[0]?.message.split("\n").slice(1)).toEqual([ + " sum (function-names.js:5): untyped JavaScript file, in a loop", + " count (function-names.js:13): untyped JavaScript file, in a loop", + " scale (function-names.js:20): untyped JavaScript file, in a loop", + " anonymous function (function-names.js:27): untyped JavaScript file, in a loop", + ]); +}); + +test("the performance warning names neither type names nor generated helpers", () => { + const { coverage } = analyze(fixture("dynamic-tier/unnamed.ts")); + expect(coverage.diagnostics).toEqual([]); + expect(coverage.performance).toBeUndefined(); +}); + test("dynamic-tier code in npm packages is summarized by package", () => { const { coverage } = analyze(fixture("dynamic-tier/package/main.ts")); expect(coverage.performance?.[0]?.message.split("\n")).toEqual([ From 61f1329b323cefdb7849ea17c7a2292849c2f6e7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 23:26:29 -0500 Subject: [PATCH 052/395] Name lowered helpers and closures independently of lowering order Lowering named helpers by the size of their registry at first use (%union.retag.N, %arr.map.N, ...), lifted closures and optional-chain locals by compiler-wide counters (%fnN, chain.N), generic instances by their instance count, and records and unions by creation order (rN, uN). Class expressions, mixin instances, function-local generics, non-exported namespace segments and some module globals carried the byte offset of their declaration. A name therefore depended on which unit lowered first and on the positions of unrelated code. Helpers, instances, records and unions are now named by a hash of the key they are interned by (keyedHelperName), and recursive records and unions by their printed type and declaration. Closures and optional-chain ids are named by their lowering unit, their structural position and a count per key within the unit. Offset-based names use the structural position: the chain of enclosing named declarations and the node's ordinal among same-kind nodes inside the nearest one, so an edit outside that declaration leaves the name unchanged. Two different keys with the same name throw an InternalCompilerError. The module lists its records and unions by id. The generated program is unchanged apart from these names and that order, and test:lowering-order now fails on any name difference. --- package.json | 2 +- packages/compiler/src/contracts/check.ts | 3 +- .../lowering/array-indexed-mutation.ts | 3 +- .../src/frontend/lowering/array-values.ts | 5 +- .../frontend/lowering/builtins/arguments.ts | 3 +- .../lowering/builtins/child-process-async.ts | 3 +- .../lowering/builtins/child-process-sync.ts | 3 +- .../lowering/builtins/disposable-stack.ts | 3 +- .../frontend/lowering/builtins/filesystem.ts | 3 +- .../src/frontend/lowering/builtins/json.ts | 3 +- .../lowering/builtins/operating-system.ts | 7 +- .../frontend/lowering/builtins/performance.ts | 5 +- .../src/frontend/lowering/builtins/process.ts | 5 +- .../frontend/lowering/builtins/promises.ts | 5 +- .../lowering/builtins/promisify-callback.ts | 3 +- .../frontend/lowering/builtins/text-codecs.ts | 3 +- .../src/frontend/lowering/builtins/timers.ts | 2 +- .../src/frontend/lowering/builtins/url.ts | 7 +- .../src/frontend/lowering/builtins/v8.ts | 3 +- .../src/frontend/lowering/call-arguments.ts | 3 +- .../src/frontend/lowering/call-signatures.ts | 7 +- .../src/frontend/lowering/class-assertions.ts | 5 +- .../src/frontend/lowering/class-unions.ts | 3 +- .../lowering/coercions/function-adapters.ts | 11 +- .../frontend/lowering/coercions/structural.ts | 15 +-- .../src/frontend/lowering/coercions/unions.ts | 15 +-- .../frontend/lowering/collection-ingestion.ts | 3 +- .../lowering/containers/array-callbacks.ts | 25 +++-- .../lowering/containers/array-concat.ts | 3 +- .../lowering/containers/array-construction.ts | 7 +- .../lowering/containers/array-methods.ts | 11 +- .../lowering/containers/array-pipelines.ts | 3 +- .../src/frontend/lowering/containers/bytes.ts | 5 +- .../lowering/containers/callback-arguments.ts | 3 +- .../containers/collection-grouping.ts | 5 +- .../lowering/containers/collection-methods.ts | 15 +-- .../lowering/containers/indexed-objects.ts | 15 +-- .../lowering/containers/iterator-helpers.ts | 7 +- .../lowering/containers/string-and-regexp.ts | 11 +- .../lowering/containers/string-replacement.ts | 3 +- .../frontend/lowering/expressions/binary.ts | 3 +- .../lowering/expressions/object-literals.ts | 7 +- .../lowering/expressions/optional-chains.ts | 10 +- .../lowering/expressions/property-access.ts | 3 +- .../lowering/expressions/relational.ts | 5 +- .../frontend/lowering/generic-functions.ts | 17 ++- .../src/frontend/lowering/intern-tables.ts | 7 +- .../frontend/lowering/iterator-adapters.ts | 17 +-- .../frontend/lowering/js-dynamic-objects.ts | 5 +- .../src/frontend/lowering/json-require.ts | 3 +- .../src/frontend/lowering/lifetime-regions.ts | 3 +- .../src/frontend/lowering/lower-assert.ts | 11 +- .../src/frontend/lowering/lower-calls.ts | 49 +++++---- .../src/frontend/lowering/lower-classes.ts | 9 +- .../src/frontend/lowering/lower-dgram.ts | 5 +- .../frontend/lowering/lower-event-emitter.ts | 13 ++- .../src/frontend/lowering/lower-exprs.ts | 9 +- .../src/frontend/lowering/lower-functions.ts | 2 +- .../src/frontend/lowering/lower-inspect.ts | 7 +- .../src/frontend/lowering/lower-mixins.ts | 3 +- .../src/frontend/lowering/lower-mock.ts | 3 +- .../frontend/lowering/lower-modules.test.ts | 4 +- .../src/frontend/lowering/lower-modules.ts | 15 ++- .../src/frontend/lowering/lower-namespaces.ts | 11 +- .../src/frontend/lowering/lower-server.ts | 13 ++- .../src/frontend/lowering/lower-stdlib.ts | 2 +- .../src/frontend/lowering/lower-stream.ts | 15 +-- .../src/frontend/lowering/lower-timers.ts | 7 +- .../src/frontend/lowering/lowerer.test.ts | 30 +++-- .../compiler/src/frontend/lowering/lowerer.ts | 100 ++++++++++++++--- .../src/frontend/lowering/native-ffi.ts | 2 +- .../lowering/object-enumeration-receiver.ts | 3 +- .../src/frontend/lowering/pairs-snapshot.ts | 3 +- .../lowering/promise-executor-adapter.ts | 3 +- .../lowering/require-initialization.ts | 3 +- .../src/frontend/lowering/stable-names.ts | 48 ++++++++ .../src/frontend/lowering/strict-equality.ts | 3 +- .../frontend/lowering/structural-positions.ts | 103 ++++++++++++++++++ packages/compiler/src/frontend/ts7/ast.ts | 90 ++++++++++++++- .../compiler/src/frontend/type-mapper.test.ts | 2 +- packages/compiler/src/frontend/type-mapper.ts | 46 ++++++-- .../test/array-pipeline-fusion.test.ts | 2 +- .../test/format-float-arguments.test.ts | 2 +- .../test/in-runtime-key-emission.test.ts | 2 +- .../compiler/test/type-mapper-unions.test.ts | 3 +- 85 files changed, 693 insertions(+), 256 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/stable-names.ts create mode 100644 packages/compiler/src/frontend/lowering/structural-positions.ts diff --git a/package.json b/package.json index 5cc30d9f80..d1a8e34ae0 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test:test262": "tsx tests/test262/run.ts", "test:test262:fetch": "node tests/test262/fetch.mjs", "test:cache-identity": "node tests/harness/cache-identity.mjs", - "test:lowering-order": "node scripts/lowering-order.mjs --fail-on content", + "test:lowering-order": "node scripts/lowering-order.mjs --fail-on names", "test:sandbox": "node scripts/sandbox-test.mjs", "test:sandbox:image": "node scripts/sandbox-image.mjs", "test:watch": "vitest", diff --git a/packages/compiler/src/contracts/check.ts b/packages/compiler/src/contracts/check.ts index 5934624bf7..c804689615 100644 --- a/packages/compiler/src/contracts/check.ts +++ b/packages/compiler/src/contracts/check.ts @@ -174,7 +174,8 @@ class ContractChecker { return `${owner}.${method[2]}`; } if (fn?.sourceName !== undefined && fn.sourceName !== "") return fn.sourceName; - if (fn !== undefined && /^%fn\d+$/.test(fnName)) return `the function at ${this.where(fn.loc)}`; + if (fn !== undefined && /^%fn\.[0-9a-f]{12}$/.test(fnName)) + return `the function at ${this.where(fn.loc)}`; return fnName.replace(/^%/, ""); } diff --git a/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts b/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts index 6d539212e2..0ded460da3 100644 --- a/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts +++ b/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts @@ -9,6 +9,7 @@ import { type SrcLoc, typeEquals, } from "../../ir/ir.js"; +import { keyedHelperName } from "./stable-names.js"; import { numLit, varRef } from "../../ir/build.js"; import { typeKey } from "../type-mapper.js"; import type { Lowerer } from "./lowerer.js"; @@ -125,7 +126,7 @@ export function lowerArrayFill( writeNull; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = "%arr.fill." + lowerer.arrHofHelpers.size; + name = keyedHelperName(lowerer, "%arr.fill", key); lowerer.arrHofHelpers.set(key, name); const a = varRef("a.0", arrType, loc); const n = varRef("n.0", F64, loc); diff --git a/packages/compiler/src/frontend/lowering/array-values.ts b/packages/compiler/src/frontend/lowering/array-values.ts index 0ca8503df3..314cdfd243 100644 --- a/packages/compiler/src/frontend/lowering/array-values.ts +++ b/packages/compiler/src/frontend/lowering/array-values.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { InternalCompilerError } from "../../errors.js"; import { dynUndefinedExpr, helperLoc, varRef } from "../../ir/build.js"; import { @@ -252,7 +253,7 @@ export function lowerSafeIndexRead( let name = lowerer.arrHofHelpers.get(key); if (!name) { const at = helperLoc(loc); - name = `%arr.idxOr.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.idxOr", key); lowerer.arrHofHelpers.set(key, name); const arrT = arr.type; lowerer.liftedFns.push({ @@ -298,7 +299,7 @@ export function lowerSafeBytesRead( let name = lowerer.arrHofHelpers.get(key); if (!name) { const at = helperLoc(loc); - name = `%bytes.idxOr.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%bytes.idxOr", key); lowerer.arrHofHelpers.set(key, name); const b = varRef("b.0", bytesT, at); const i = varRef("i.0", F64, at); diff --git a/packages/compiler/src/frontend/lowering/builtins/arguments.ts b/packages/compiler/src/frontend/lowering/builtins/arguments.ts index 7f862f6031..d2fc430dd4 100644 --- a/packages/compiler/src/frontend/lowering/builtins/arguments.ts +++ b/packages/compiler/src/frontend/lowering/builtins/arguments.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { BYTES_U8, BOOL, @@ -72,7 +73,7 @@ export function lowerOptionalNumberPredicate( const key = `number.optionalPredicate:${fn}:${typeKey(widened)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%number.optionalPredicate.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%number.optionalPredicate", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", widened, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts index 5590fbe9fa..d0991dce5c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { recordToEnvPairs } from "./environment.js"; import { lowerChildArgsArg } from "./child-process.js"; import { boolLit, numLit, varRef } from "../../../ir/build.js"; @@ -218,7 +219,7 @@ function execFileAsyncHelper( const key = shell ? "execAsync" : "execFileAsync"; const existing = lowerer.valueHelpers.get(key); if (existing) return { name: existing, shapeId, envShapeId }; - const name = `%${key}.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, `%${key}`, key); lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; const envRecT: IrType = { kind: "record", shapeId: envShapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts index 24adcc1ba1..b9858e7681 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { recordToEnvPairs } from "./environment.js"; import { lowerChildArgsArg } from "./child-process.js"; import { boolLit, numLit, strLit, varRef } from "../../../ir/build.js"; @@ -541,7 +542,7 @@ function execStdioModeHelper( const key = `cp.stdiomode:${shapeId}:${fd}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%cp.stdioMode.${fd}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%cp.stdioMode.${fd}`, key); lowerer.arrHofHelpers.set(key, name); const optsT: IrType = { kind: "record", shapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts index 1b9a9c03a8..375c3f1e72 100644 --- a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts +++ b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; @@ -84,7 +85,7 @@ export function lowerDisposableStackUseCall( const key = `disposableStack.use:${className}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%disposableStack.use.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%disposableStack.use", key); lowerer.valueHelpers.set(key, helper); const context = newFnCtx(false, null, null, VOID); lowerer.fnStack.push(context); diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index 0e1c062c50..6b988c491d 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { dynUndefinedExpr, boolLit, numLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, ladderFenceExpr } from "../lowerer.js"; @@ -442,7 +443,7 @@ export function lowerStringOrBytesWrite( const key = `fs.${append ? "append" : "write"}:${dataType.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%fs.writeData.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%fs.writeData", key); lowerer.valueHelpers.set(key, helper); const body: IrStmt[] = []; for (let tag = 0; tag < arms.length; tag++) { diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index 7eb8aa9b2c..64f829865d 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { isDateJsonSource } from "./dates.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; @@ -253,7 +254,7 @@ function lowerOptionalStringifyRoot( const key = `json.optionalString:${value.type.unionId}:${JSON.stringify(indent)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%json.optionalString.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%json.optionalString", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", value.type, loc); const serialized: IrExpr = { diff --git a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts index c104650393..ee189a1f35 100644 --- a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts +++ b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { numLit, strLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; @@ -219,7 +220,7 @@ export function lowerOsCpusCall(lowerer: Lowerer, call: ts.CallExpression, loc: const key = `os.cpus:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%os.cpus.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%os.cpus", key); lowerer.valueHelpers.set(key, helper); const index = varRef("i.0", F64, loc); const field = (k: number): IrExpr => ({ @@ -346,7 +347,7 @@ export function lowerPathParseCall( const key = `${fn}:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%path.parse.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%path.parse", key); lowerer.valueHelpers.set(key, helper); const parts = varRef("parts.0", arrayOf(STRING), loc); lowerer.liftedFns.push({ @@ -448,7 +449,7 @@ export function lowerPathFormatCall( const key = `${fn}:${typeKey(recordT)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%path.format.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%path.format", key); lowerer.valueHelpers.set(key, helper); const object = varRef("object.0", recordT, loc); const read = (name: string): IrExpr => { diff --git a/packages/compiler/src/frontend/lowering/builtins/performance.ts b/packages/compiler/src/frontend/lowering/builtins/performance.ts index 72304e6a90..bae525c17c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/performance.ts +++ b/packages/compiler/src/frontend/lowering/builtins/performance.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; @@ -330,7 +331,7 @@ function lowerPerfTimelineCall( const key = `perf.${many ? "entries" : timeline.fn}:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%perf.${many ? "entries" : member}.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%perf.${many ? "entries" : member}`, key); lowerer.valueHelpers.set(key, helper); const argsRef = varRef("a.0", DYN, loc); const whichRef = varRef("w.0", F64, loc); @@ -461,7 +462,7 @@ export function perfEntryTag( const key = `perf.tag:${entryT.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%perf.tag.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%perf.tag", key); lowerer.valueHelpers.set(key, helper); const e = varRef("e.0", entryT, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 25653f5419..842f42b579 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { stdinKeypressListener } from "./readline.js"; import { envSnapshotHelper, lowerProcessLoadEnvFile } from "./environment.js"; import { dynUndefinedExpr, boolLit, numLit, strLit, varRef } from "../../../ir/build.js"; @@ -1561,7 +1562,7 @@ export function lowerProcessMethodCall( const key = `${prefix}usage.diff:${prevT.shapeId}:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%${prefix}usage.diff.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%${prefix}usage.diff`, key); lowerer.valueHelpers.set(key, helper); const pRef: IrExpr = { kind: "varRef", localId: "p.0", type: prevT, loc }; const fieldOf = (name: string): IrExpr => ({ @@ -1636,7 +1637,7 @@ export function lowerProcessMethodCall( const key = `process.memoryUsage:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%process.memoryUsage.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%process.memoryUsage", key); lowerer.valueHelpers.set(key, helper); lowerer.liftedFns.push({ name: helper, diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 668ef01f8c..75e527aa5d 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { checkedPromiseAll, thenReader } from "../checked-promise-all.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -55,7 +56,7 @@ function lowerTuplePromiseAll( const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(resultT.inner)}`; let name = lowerer.arrHofHelpers.get(`promise.allTuple:${key}`); if (name === undefined) { - name = `%promise.allTuple.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%promise.allTuple", `promise.allTuple:${key}`); lowerer.arrHofHelpers.set(`promise.allTuple:${key}`, name); const ctx = newFnCtx(false, null, null, resultT.inner); ctx.isAsync = true; @@ -557,7 +558,7 @@ export function lowerPromiseStaticCall( const key = `promise.try:${typeKey(f.type)}:${args.map((a) => typeKey(a.type)).join(",")}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%promise.try.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%promise.try", key); lowerer.arrHofHelpers.set(key, helper); const fRef: IrExpr = { kind: "varRef", localId: "f.0", type: f.type, loc }; const argLocals = args.map((a, i) => ({ id: `a${i}.0`, name: `a${i}`, type: a.type })); diff --git a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts index 822a4fecc3..b4dbbf5137 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { varRef } from "../../../ir/build.js"; @@ -170,7 +171,7 @@ function promisifyMaker(lowerer: Lowerer, shape: CallbackShape, loc: SrcLoc): st const key = `promisify:${typeKey(shape.fnT)}=>${typeKey(shape.resultT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const base = `%promisify.${lowerer.arrHofHelpers.size}`; + const base = keyedHelperName(lowerer, "%promisify", key); lowerer.arrHofHelpers.set(key, base); const { fnT, resultT, args, callbackT, callbackParamT, inner, error } = shape; const promiseT = resultT.ret; diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index 7229d5b84f..8d47c8e3af 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { isBuiltinClassInstance } from "./receiver-types.js"; import { strLit, varRef, dynUndefinedExpr } from "../../../ir/build.js"; import { staticTextDecoderEncoding } from "../text-decoder-encoding.js"; @@ -129,7 +130,7 @@ function stringDecoderHelper( const key = `strdec.${op}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%strdec.${op}.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, `%strdec.${op}`, key); lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts index 2d531663f3..d7cbd63b50 100644 --- a/packages/compiler/src/frontend/lowering/builtins/timers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts @@ -85,7 +85,7 @@ export function lowerTimersPromisesSetInterval( const genT = timersPromisesIntervalType(lowerer, expr); if (genT === null) lowerer.badType(expr, lowerer.typeOf(expr)); const yielded = lowerer.coerceInto(valueNode, value, genT.yieldT); - const fnName = `%fn${lowerer.lambdaCounter++}_tpInterval`; + const fnName = lowerer.closureName(loc, "_tpInterval"); const msParam: IrLocal = { id: "%tp.ms", name: "delay", type: F64, mutable: false }; const valueParam: IrLocal = { id: "%tp.value", name: "value", type: genT.yieldT, mutable: false }; const promiseVoid: IrType = { kind: "promise", inner: VOID }; diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts index dca8a7cc00..2a77fa85c1 100644 --- a/packages/compiler/src/frontend/lowering/builtins/url.ts +++ b/packages/compiler/src/frontend/lowering/builtins/url.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { nodeThrowExpr, countedFor, strLit, varRef, dynUndefinedExpr } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; @@ -292,7 +293,7 @@ function searchParamsFromTuples( const key = `sp.fromTuples:${row.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%sp.fromTuples.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%sp.fromTuples", key); lowerer.valueHelpers.set(key, helper); const pairsT = init.type; const sp = (): IrExpr => varRef("sp.0", SEARCH_PARAMS_T, loc); @@ -372,7 +373,7 @@ function lowerOptionalStringSearchParams( const key = `sp.optionalString:${init.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%sp.optionalString.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%sp.optionalString", key); lowerer.valueHelpers.set(key, helper); const value = varRef("init.0", init.type, loc); lowerer.liftedFns.push({ @@ -678,7 +679,7 @@ function lowerSpForEachCall( const key = `${arity}:${typeKey(fnRet)}`; let helper = lowerer.spHofHelpers.get(key); if (!helper) { - helper = `%sp.forEach.${lowerer.spHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%sp.forEach", key); lowerer.spHofHelpers.set(key, helper); lowerer.liftedFns.push(buildSpForEachFn(helper, arity, fnRet, loc)); } diff --git a/packages/compiler/src/frontend/lowering/builtins/v8.ts b/packages/compiler/src/frontend/lowering/builtins/v8.ts index 3582ce8aeb..989a61a3f0 100644 --- a/packages/compiler/src/frontend/lowering/builtins/v8.ts +++ b/packages/compiler/src/frontend/lowering/builtins/v8.ts @@ -5,6 +5,7 @@ * spirit of process.memoryUsage(): approximate values, not V8 heap * figures (a compiled binary embeds no V8 engine). Every other member * fences at its use with the by-design reason (builtinFenceHintOf). */ +import { keyedHelperName } from "../stable-names.js"; import { numLit } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; @@ -55,7 +56,7 @@ export function lowerV8HeapStatisticsCall( const key = `v8.getHeapStatistics:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%v8.getHeapStatistics.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%v8.getHeapStatistics", key); lowerer.valueHelpers.set(key, helper); const sample = (index: number): IrExpr => ({ kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index 49f7c8ec0f..060ffe9d24 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { forwardedTupleType, pureForwardedValues, stripTypeWrappers } from "./typed-arguments.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; @@ -753,7 +754,7 @@ function holeReadingLift(lowerer: Lowerer, from: IrType, to: IrType, loc: SrcLoc const key = `arr.holes:${typeKey(elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%arr.holes.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.holes", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push( buildArrayConversion( diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 34343ad270..dacb4d3246 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -849,7 +849,10 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara lowerer.genericFnsBySymbol.set(symbol, { decl, baseName: nameText, - qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText), + qualifiedName: lowerer.qualify( + ts.getSourceFileOfNode(decl), + nsPathPrefix(lowerer, decl) + nameText, + ), typeParams: [], instances: new Map(), implicitParams: implicit, @@ -897,7 +900,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara // "%default" spelling ('%' cannot appear in a user identifier). name: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(decl) + (decl.name?.text ?? "%default"), + nsPathPrefix(lowerer, decl) + (decl.name?.text ?? "%default"), ), params, returnType, diff --git a/packages/compiler/src/frontend/lowering/class-assertions.ts b/packages/compiler/src/frontend/lowering/class-assertions.ts index 900c324262..52654fab8a 100644 --- a/packages/compiler/src/frontend/lowering/class-assertions.ts +++ b/packages/compiler/src/frontend/lowering/class-assertions.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { nodeThrowExpr, varRef } from "../../ir/build.js"; import { BOOL, @@ -122,7 +123,7 @@ export function checkedClassAssertion( const key = `classassert:${typeKey(source.type)}:${typeKey(target)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%class.assert.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%class.assert", key); lowerer.coercions.retags.set(key, name); const input = varRef("value.0", source.type, loc); const body: IrStmt[] = []; @@ -228,7 +229,7 @@ export function checkedClassUnionAssertion( const key = `classunionassert:${typeKey(value.type)}:${typeKey(target)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%class.assert.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%class.assert", key); lowerer.coercions.retags.set(key, name); const input = varRef("value.0", value.type, loc); const locals = [{ id: "value.0", name: "value", type: value.type, mutable: false }]; diff --git a/packages/compiler/src/frontend/lowering/class-unions.ts b/packages/compiler/src/frontend/lowering/class-unions.ts index f52026ba42..0021b2cf89 100644 --- a/packages/compiler/src/frontend/lowering/class-unions.ts +++ b/packages/compiler/src/frontend/lowering/class-unions.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { BOOL, typeKey, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; import { nodeThrowExpr, varRef } from "../../ir/build.js"; @@ -132,7 +133,7 @@ export function narrowClassUnion( const key = `classUnion:${source.className}:${target.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%class.union.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%class.union", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", source, loc); // Rank by ancestry before sorting: returning zero for unrelated pairs diff --git a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts index ffdac19f0d..fe3c14605c 100644 --- a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts +++ b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; import { buildFunctionAdapter } from "./builders.js"; import { InternalCompilerError } from "../../../errors.js"; @@ -33,7 +34,7 @@ export function funcReturnWidthAdapter( const key = `fn:${typeKey(fromT.ret.elem)}:${typeKey(toT.ret.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%fn.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%fn.width", key); lowerer.valueHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); // The returned closure's body: call the captured original, width-map. @@ -163,7 +164,7 @@ function restPackingAdapter( const key = `fnrestadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%fn.adapt.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%fn.adapt", key); lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const fixed = fromT.params.length - 1; @@ -220,7 +221,7 @@ function funcIntoDynRestAdapter( const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%fn.adapt.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%fn.adapt", key); lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = [{ localId: "rest.0", name: "args", type: DYN }]; @@ -376,7 +377,7 @@ export function funcCoerceAdapter( const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%fn.adapt.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%fn.adapt", key); lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = toT.params.map((t, i) => ({ @@ -604,7 +605,7 @@ export function spawnResFnAdapter( const key = `fnspawn:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%fnval.spawnres.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%fnval.spawnres", key); lowerer.valueHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = toT.params.map((p, i) => ({ diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 496a296074..5b5e283b32 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { constructedValue } from "../constructor-returns.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import type { FieldLift } from "./structural-plans.js"; @@ -253,7 +254,7 @@ export function unionRecordWidthHelper( const key = `unionWidth:${fromId}:${target.shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%union.record.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%union.record", key); lowerer.valueHelpers.set(key, name); const type: IrType = { kind: "union", unionId: fromId }; const value = varRef("value.0", type, loc); @@ -312,7 +313,7 @@ export function recordWidthHelper( const key = `rec:${fromId}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%rec.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%rec.width", key); // Interned BEFORE the body builds: a recursive nested-width field // (self-referential shapes) resolves to this helper itself. lowerer.valueHelpers.set(key, name); @@ -341,7 +342,7 @@ export function tupleArrayWidthHelper( const key = `tuparr:${fromId}:${typeKey(toT.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%tup.arr.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%tup.arr", key); lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "record", shapeId: fromId }; const t: IrExpr = { kind: "varRef", localId: "t.0", type: fromT, loc }; @@ -388,7 +389,7 @@ export function emptyArrayLiftHelper( const key = `emptyarr:${typeKey(fromT.elem)}:${typeKey(toT.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%arr.empty.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.empty", key); lowerer.valueHelpers.set(key, name); const a: IrExpr = { kind: "varRef", localId: "a.0", type: fromT, loc }; lowerer.liftedFns.push({ @@ -452,7 +453,7 @@ export function arrayWidthHelper( const key = `arr:${typeKey(fromElem)}:${typeKey(toElem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%arr.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.width", key); lowerer.valueHelpers.set(key, name); const arrT: IrType = { kind: "array", elem: fromElem }; const outT: IrType = { kind: "array", elem: toElem }; @@ -492,7 +493,7 @@ export function objRecordWidthHelper( const key = `obj:${className}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%obj.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%obj.width", key); lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "object", className }; lowerer.liftedFns.push(buildRecordProjection(lowerer, name, fromT, to, plan, loc)); @@ -517,7 +518,7 @@ export function recordClassWidthHelper( const key = `cls:${fromId}:${className}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%cls.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%cls.width", key); lowerer.valueHelpers.set(key, name); lowerer.noteEdge(`%${className}.constructor`); const fromT: IrType = { kind: "record", shapeId: fromId }; diff --git a/packages/compiler/src/frontend/lowering/coercions/unions.ts b/packages/compiler/src/frontend/lowering/coercions/unions.ts index 06c0af9c6b..434e687a50 100644 --- a/packages/compiler/src/frontend/lowering/coercions/unions.ts +++ b/packages/compiler/src/frontend/lowering/coercions/unions.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { buildUnionNarrow, unionNarrowFailureMessage } from "../union-narrow.js"; import { isSafeToRepeat } from "../expressions/evaluation-safety.js"; import { nodeThrowExpr } from "../../../ir/build.js"; @@ -147,7 +148,7 @@ export function strandedUnitTrap( const key = `strandunit:${typeKey(expected)}:${expr.type.kind}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%unit.strand.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%unit.strand", key); lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push({ name, @@ -227,7 +228,7 @@ export function strandedCoercionTrap( const key = `strand:${expected.unionId}:${typeKey(src)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%union.strand.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%union.strand", key); lowerer.coercions.retags.set(key, name); const toT: IrType = { kind: "union", unionId: expected.unionId }; lowerer.liftedFns.push({ @@ -275,7 +276,7 @@ export function recordUnionWrapHelper( const key = `recordWrap:${source.shapeId}:${toId}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%record.wrap.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%record.wrap", key); lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) => @@ -307,7 +308,7 @@ export function recordUnionRouteHelper( (from, dst) => lowerer.widthLiftPlan(from, dst), ); if (!plan) return null; - const name = `%record.route.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%record.route", key); lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) => @@ -352,7 +353,7 @@ export function unionRetagHelper( }); const key = `${fromId}:${toId}:${stranded.join(".")}`; const existing = lowerer.coercions.retags.get(key); - const name = existing ?? `%union.retag.${lowerer.coercions.retags.size}`; + const name = existing ?? keyedHelperName(lowerer, "%union.retag", key); if (!existing) { lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( @@ -410,7 +411,7 @@ export function narrowedArmHelper( lowerer.coercions.checkedNarrows.add(existing); return existing; } - const name = `%union.narrow.${lowerer.coercions.narrows.size}`; + const name = keyedHelperName(lowerer, "%union.narrow", key); lowerer.coercions.narrows.set(key, name); lowerer.coercions.checkedNarrows.add(name); const fn = buildUnionNarrow(name, from, target, loc, (type) => lowerer.fmt(type)); @@ -587,7 +588,7 @@ export function deferredReadHelper( const key = `deferred:${fromId}:${tag}`; const existing = lowerer.coercions.narrows.get(key); if (existing) return existing; - const name = `%deferred.read.${lowerer.coercions.narrows.size}`; + const name = keyedHelperName(lowerer, "%deferred.read", key); lowerer.coercions.narrows.set(key, name); const dflt: IrExpr = target.kind === "bool" diff --git a/packages/compiler/src/frontend/lowering/collection-ingestion.ts b/packages/compiler/src/frontend/lowering/collection-ingestion.ts index 5d14d54925..8fe2a6bbbf 100644 --- a/packages/compiler/src/frontend/lowering/collection-ingestion.ts +++ b/packages/compiler/src/frontend/lowering/collection-ingestion.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { InternalCompilerError } from "../../errors.js"; import { typeKey } from "../type-mapper.js"; @@ -197,7 +198,7 @@ export function ingestCollection( const key = `ingest:${input.collection ? typeKey(input.collection) + ":" + input.selection : "cursor:" + typeKey(input.element)}:${typeKey(destination)}:${mapper ? typeKey(mapper.type) : "copy"}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%collection.ingest.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%collection.ingest", key); lowerer.arrHofHelpers.set(key, name); const context = newFnCtx(false, null, null, destination); lowerer.fnStack.push(context); diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index eb99592bb9..7ca69d9ba3 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js"; import { transformStmtList } from "../../../ir/traverse.js"; import type { ScrDiagnostic } from "../../../diagnostics/diagnostic.js"; @@ -506,7 +507,7 @@ function arrayHofHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}:${typeKey(outElem)}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method, elem, fnRet, arity, outElem, present, split: !!fastT }); const fn = buildArrayHofFn( @@ -619,7 +620,7 @@ function arrayUnionHofHelper( const key = `union:${method}:${receiverT.unionId}:${typeKey(callbackT)}:${typeKey(resultT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.union.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.union", key); lowerer.arrHofHelpers.set(key, name); const receiver = varRef("a.0", receiverT, loc); const callback = varRef("f.0", callbackT, loc); @@ -1204,7 +1205,7 @@ function findHelper( const key = `${method}:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}${site}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); const valueT = callbackValueType(lowerer, elem, present); @@ -1304,7 +1305,7 @@ function findIndexHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); if (!last) lowerer.arrHofInfo.set(name, { @@ -1388,7 +1389,7 @@ function someEveryHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method, @@ -1587,7 +1588,7 @@ function flatMapHelper( const key = `flatMap:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.flatMap.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.flatMap", key); lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method: "flatMapArray", @@ -1829,7 +1830,7 @@ function reduceHelper( const key = `${method}:${typeKey(elem)}:${typeKey(accT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : "seed"}${site}${fastT ? ":split" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); if (method === "reduce") lowerer.arrHofInfo.set(name, { @@ -2121,7 +2122,7 @@ export function lowerArraySortCall( const key = `${method}:${typeKey(elem)}:${arity}${native ? ":native" : ""}${keyed ? `:keyed${order}${reads ? `:field${reads.length}` : ""}` : ""}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const undefinedTag = elem.kind === "union" ? lowerer.armTag(elem.unionId, UNDEFINED_T) : -1; lowerer.liftedFns.push( @@ -2167,7 +2168,7 @@ function sortKeyReader( const cacheKey = `sortKey:${typeKey(elem)}:${sortKeysId(reads.map((read) => ({ order: 1, read })))}`; let reader = lowerer.arrHofHelpers.get(cacheKey); if (!reader) { - reader = `%arr.sortKey.${lowerer.arrHofHelpers.size}`; + reader = keyedHelperName(lowerer, "%arr.sortKey", cacheKey); lowerer.arrHofHelpers.set(cacheKey, reader); lowerer.liftedFns.push(buildSortKeyFn(reader, elem, reads, loc)); } @@ -2308,7 +2309,7 @@ function dynFlatMapHelper(lowerer: Lowerer, elemT: IrType, arity: number, loc: S const key = `dynflatmap:${typeKey(elemT)}:${arity}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%dyn.flatmap.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%dyn.flatmap", key); lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push(buildDynFlatMapFn(name, elemT, arity, loc)); return name; @@ -2481,7 +2482,7 @@ function dynFilterHelper(lowerer: Lowerer, elemT: IrType, arity: number, loc: Sr const key = `dynfilter:${typeKey(elemT)}:${arity}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%dyn.filter.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%dyn.filter", key); lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push(buildDynFilterFn(name, elemT, arity, loc)); return name; @@ -2676,7 +2677,7 @@ export function lowerTuplePredicateCall( const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(fnArg.type)}:${typeKey(resultT)}:${arity}${callbackSite(fnArg)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%tuple.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const v = varRef("v.0", elem, loc); const out = varRef("out.0", resultT, loc); diff --git a/packages/compiler/src/frontend/lowering/containers/array-concat.ts b/packages/compiler/src/frontend/lowering/containers/array-concat.ts index 4f6e4ca437..b4e285ee98 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-concat.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-concat.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { identityPreservingWidening } from "../coercions/identity.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import { @@ -28,7 +29,7 @@ export function arrayConcatHelper( const key = `concat:${typeKey(elem)}:${shape.map((type) => (type ? `a:${typeKey(type)}` : "e")).join(";")}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.concat.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.concat", key); const arrT = arrayOf(elem); const params = [ { localId: "a.0", name: "a", type: arrT }, diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 40e4d64367..1f10640c03 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { generatorDrain } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; import { liftedFreshSpread } from "../coercions/fresh-spread.js"; @@ -256,7 +257,7 @@ function appendWidenedArrayValues( const key = `insertValues:${typeKey(sourceType.elem)}:${typeKey(elem)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%arr.insertValues.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.insertValues", key); lowerer.arrHofHelpers.set(key, name); const outType = arrayOf(elem); const valueType = arrayValueType(lowerer, source.type.elem); @@ -1035,7 +1036,7 @@ export function lowerArrayFromCall( const key = `fromArray:${typeKey(arrT.elem)}:${typeKey(outElem)}:${typeKey(fnRet)}:${arity}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%arr.fromArray.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%arr.fromArray", key); lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push( buildArrayFromArrayFn(lowerer, helper, arrT.elem, outElem, fnRet, arity, loc), @@ -1122,7 +1123,7 @@ export function lowerArrayFromCall( const key = `fromLen:${typeKey(fnRet)}:${arity}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%arr.fromLen.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%arr.fromLen", key); lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push(buildArrayFromLenFn(helper, fnRet, arity, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index e9a130a2db..44182fbda6 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { liftedFreshSpread } from "../coercions/fresh-spread.js"; import { lowerArrayIteratorValue } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; @@ -1616,7 +1617,7 @@ function arraySearchHelper( const key = `${method}:${typeKey(elem)}:${typeKey(valueT)}:${typeKey(needleT)}:${directNeedle}:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); const resultT = method === "includes" ? BOOL : F64; @@ -1972,7 +1973,7 @@ export function lowerTupleReadMethodCall( let helper = lowerer.arrHofHelpers.get(key); const resultType = method === "includes" ? BOOL : F64; if (!helper) { - helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%tuple.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const params = [ { localId: "a.0", name: "a", type: receiver.type }, @@ -2034,7 +2035,7 @@ export function lowerTupleReadMethodCall( const key = `tuple.slice:${typeKey(receiver.type)}:${args.length}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%tuple.slice.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%tuple.slice", key); lowerer.arrHofHelpers.set(key, helper); const params = [receiver, ...args].map((arg, i) => ({ localId: i === 0 ? "a.0" : `arg.${i}`, @@ -2125,7 +2126,7 @@ export function lowerTupleReadMethodCall( const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(fnArg.type)}:${typeKey(outElem)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%tuple.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const out = varRef("out.0", outType, loc); const result = varRef("r.0", ret, loc); @@ -2253,7 +2254,7 @@ function lowerArrayAtCall( const key = `at:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(index.type)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%arr.at.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.at", key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index cf45a75751..180f404351 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js"; import { BOOL, @@ -851,7 +852,7 @@ class PipelineFusion { if (!built) return null; let name = this.helpers.get(built.key); if (!name) { - name = `%arr.fused.${this.helpers.size}`; + name = keyedHelperName(this.lowerer, "%arr.fused", built.key); this.helpers.set(built.key, name); this.added.push({ ...built.fn, name }); } diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index bf9821d317..4b20d7bf55 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, dynUndefinedExpr, numLit, varRef } from "../../../ir/build.js"; import { BUF_NUM_METHODS } from "./buffer-numeric-methods.js"; import * as ts from "../../ts7/adapter.js"; @@ -77,7 +78,7 @@ function lowerBytesSortCall( const cacheKey = `bytes.${method}:${bytesT.elem}:${key}`; let helper = lowerer.arrHofHelpers.get(cacheKey); if (!helper) { - helper = `%bytes.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%bytes.${method}`, cacheKey); lowerer.arrHofHelpers.set(cacheKey, helper); lowerer.liftedFns.push( buildBytesSortFn(helper, bytesT, arity, hasComparator, copyFirst, order, loc), @@ -270,7 +271,7 @@ function bytesCallbackHelper( const key = `bytes.${method}:${bytesT.elem}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%bytes.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%bytes.${method}`, key); lowerer.arrHofHelpers.set(key, name); const reduce = method === "reduce" || method === "reduceRight"; const backward = method === "reduceRight" || method === "findLast" || method === "findLastIndex"; diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index 18754f0881..f39c8e86c7 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; import { isJsSourceFile, locOf } from "../../program.js"; @@ -285,7 +286,7 @@ export function truthyFilterCallback( const key = `filterBoolean:${typeKey(valueT)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%arr.filterBoolean.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.filterBoolean", key); lowerer.arrHofHelpers.set(key, name); const value = varRef("v.0", valueT, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts index 0bf1268301..dae68d518a 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; @@ -104,7 +105,7 @@ export function lowerGroupByStaticCall( const key = `map.groupBy:${typeKey(items.type)}:${typeKey(f.type)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.groupBy.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.groupBy", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildGroupByFn(lowerer, helper, items.type, f.type, resultT, loc)); } @@ -141,7 +142,7 @@ export function lowerGroupByStaticCall( const key = `object.groupBy:${typeKey(items.type)}:${typeKey(f.type)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%object.groupBy.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%object.groupBy", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildGroupByFn(lowerer, helper, items.type, f.type, resultT, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index 370974c85c..d3169ec5cb 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { nodeThrowExpr, boolLit, countedFor, numLit, strLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -232,7 +233,7 @@ function lowerMapIterDrainCall( const key = `${method}:${typeKey(mapT.key)}:${typeKey(mapT.value)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.${method}.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%map.${method}`, key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildMapIterDrainFn(helper, mapT, method, elemT, tupleT, loc)); } @@ -348,7 +349,7 @@ export function mapEntriesArray(lowerer: Lowerer, receiver: IrExpr): IrExpr { const key = `entries:${typeKey(mapT.key)}:${typeKey(mapT.value)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.entries.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.entries", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push( buildMapIterDrainFn(helper, mapT, "entries", tupleT, tupleT, receiver.loc), @@ -519,7 +520,7 @@ export function lowerMapForEachCall( const key = `${typeKey(mapT.key)}:${typeKey(mapT.value)}:${arity}:${typeKey(fnRet)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.forEach.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.forEach", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildCollectionForEachFn(helper, mapT, arity, fnRet, loc)); } @@ -693,7 +694,7 @@ export function lowerSetForEachCall( const key = `${typeKey(setT.elem)}:${arity}:${typeKey(fnRet)}`; let helper = lowerer.setHofHelpers.get(key); if (!helper) { - helper = `%set.forEach.${lowerer.setHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%set.forEach", key); lowerer.setHofHelpers.set(key, helper); lowerer.liftedFns.push(buildCollectionForEachFn(helper, setT, arity, fnRet, loc)); } @@ -745,7 +746,7 @@ function lowerSetCombineCall( const key = `${name}:${typeKey(setT.elem)}:${typeKey(otherT)}`; let helper = lowerer.setHofHelpers.get(key); if (!helper) { - helper = `%set.${name}.${lowerer.setHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%set.${name}`, key); lowerer.setHofHelpers.set(key, helper); lowerer.liftedFns.push(buildSetCombineFn(helper, name, setT, otherT, loc)); } @@ -1357,7 +1358,7 @@ export function mapFromSeedValue( const key = `checked-seed:${typeKey(mapT)}`; let name = lowerer.mapHofHelpers.get(key); if (!name) { - name = `%map.checkedSeed.${lowerer.mapHofHelpers.size}`; + name = keyedHelperName(lowerer, "%map.checkedSeed", key); lowerer.mapHofHelpers.set(key, name); const source = varRef("source.0", DYN, loc); const entries = varRef("entries.0", DYN, loc); @@ -1457,7 +1458,7 @@ export function mapFromSeedValue( const key = `seed:${typeKey(mapT.key)}:${typeKey(mapT.value)}:${elem.shapeId}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.seed.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.seed", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildMapSeedFn(mapT, helper, seed.type, elem, kT, vT, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index efc173ffca..b3faf649eb 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import { objectEnumerationReceiver } from "../object-enumeration-receiver.js"; @@ -184,7 +185,7 @@ export function objectIterOverIndexShape( const key = `obj.${member}:ovf:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.${member}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%obj.${member}`, key); const outRef = varRef("out.0", resultT, loc); const rRef = varRef("r.0", argIr, loc); @@ -608,7 +609,7 @@ function lowerTypedObjectFromEntriesCall( const key = `obj.fromEntries:${argIr.elem.shapeId}:${resultT.shapeId}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.fromEntries.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.fromEntries", key); const tupleT = argIr.elem as IrType & { kind: "record" }; const tRef = varRef("t.0", tupleT, loc); @@ -712,7 +713,7 @@ function lowerFromEntriesStringRows( const key = `obj.fromEntries:strrows:${resultT.shapeId}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.fromEntries.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.fromEntries", key); const tRef = varRef("t.0", rowT, loc); const rowLen: IrExpr = { @@ -931,7 +932,7 @@ export function lowerRecordOvfCaptureHelper( const key = `ovf:${fromId}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%rec.capture.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%rec.capture", key); lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "record", shapeId: fromId }; const toT: IrType = { kind: "record", shapeId: toId }; @@ -1204,7 +1205,7 @@ export function lowerObjectAssignIndexShape( const key = `assign:${targetIr.shapeId}:${plan.fromId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%obj.assign.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%obj.assign", key); lowerer.valueHelpers.set(key, name); const toT: IrType = { kind: "record", shapeId: targetIr.shapeId }; const fromT: IrType = { kind: "record", shapeId: plan.fromId }; @@ -1474,7 +1475,7 @@ export function lowerIndexMergeHelper( .join(","); const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%rec.merge.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%rec.merge", key); lowerer.valueHelpers.set(key, name); const toT: IrType = { kind: "record", shapeId: toId }; const ksT = arrayOf(STRING); @@ -1805,7 +1806,7 @@ export function lowerEnvToPairsHelper( const key = `env.pairs:${shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%env.pairs.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%env.pairs", key); lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; const arrT = arrayOf(STRING); diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index c23e6931c6..48b4caabdb 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, numLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -966,7 +967,7 @@ function lowerFusedChain( const key = `iter:${srcKey}:${stageKeys.join(",")}:${terminal}:${termArgs.map((a) => typeKey(a.type)).join(",")}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%iter.${terminal}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%iter.${terminal}`, key); lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push( buildIterChainFn( @@ -1038,7 +1039,7 @@ function lowerHelperObject( const key = `iterHelper:${stage.kind}:${srcKey}:${typeKey(srcElem)}:${typeKey(arg.type)}:${typeKey(outT)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%iter.${stage.kind}.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, `%iter.${stage.kind}`, key); lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( buildHelperGenFn(lowerer, name, source, srcElem, stage, arg.type, outT, resultType, loc), @@ -1058,7 +1059,7 @@ function internBudgetChecker(lowerer: Lowerer, loc: SrcLoc, srcT: GenType | null const key = srcT === null ? "iter:budget" : `iter:budget:close:${typeKey(srcT)}`; let name = lowerer.arrHofHelpers.get(key); if (name !== undefined) return name; - name = srcT === null ? "%iter.budget" : `%iter.budget.close.${lowerer.arrHofHelpers.size}`; + name = srcT === null ? "%iter.budget" : keyedHelperName(lowerer, "%iter.budget.close", key); lowerer.arrHofHelpers.set(key, name); const v = (): IrExpr => ({ kind: "varRef", localId: "v.0", type: F64, loc }); diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 9567ee0f6c..fe4ddbc6c3 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { matchAllRowsReadByElements } from "./match-row-uses.js"; import { boolLit, @@ -159,7 +160,7 @@ export function lowerStringSplitCall( const key = `str.split:${receiver.type.kind === "union" ? typeKey(receiver.type) : receiver.type.kind}:${typeKey(separator.type)}:${typeKey(limit.type)}:${absent}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.split.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.split", key); const values = [receiver, separator, limit]; const params = values.map((value, index) => ({ localId: `arg.${index}`, @@ -395,7 +396,7 @@ function stringIndexHelperCall( const key = `str.index:${method}:${typeKey(receiver.type)}:${typeKey(index.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.index.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.index", key); lowerer.valueHelpers.set(key, helper); const rawReceiver = varRef("arg.0", receiver.type, loc); const rawIndex = varRef("arg.1", index.type, loc); @@ -673,7 +674,7 @@ export function lowerStringPaddingCall( const key = `str.pad:${method}:${values.map((value) => typeKey(value.type)).join(":")}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.pad.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.pad", key); const params = values.map((value, index) => ({ localId: `arg.${index}`, name: `arg${index}`, @@ -1309,7 +1310,7 @@ export function lowerStringMethodCall( const key = `str.positions:${entry.method}:${typeKey(needle.type)}:${typeKey(position.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.positions.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.positions", key); const params = [receiver, needle, position].map((arg, index) => ({ localId: `arg.${index}`, name: `arg${index}`, @@ -1411,7 +1412,7 @@ export function lowerStringMethodCall( const key = `str.positions:${entry.method}:${args.map((arg) => typeKey(arg.type)).join(":")}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.positions.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.positions", key); const params = [receiver, ...args].map((arg, index) => ({ localId: `arg.${index}`, name: `arg${index}`, diff --git a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts index 6856bc50bb..780c77d0f0 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { BOOL, @@ -162,7 +163,7 @@ function lowerStringReplacementValues( const key = `str.${method}:${typeKey(replacement.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.${method}.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%str.${method}`, key); lowerer.valueHelpers.set(key, helper); const subject = varRef("s.0", STRING, loc); const needle = varRef("search.0", STRING, loc); diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index fafbbf695a..7e449f12b7 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -1,4 +1,5 @@ /* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { cleanFuncAdaptable } from "../coercions/function-adapters.js"; import { type Lowerer, jsFuncNameOf } from "../lowerer.js"; @@ -801,7 +802,7 @@ function nullProtoToString(lowerer: Lowerer, e: IrExpr, t: IrType & { kind: "rec const key = `nullProtoToString:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%rec.tostr.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%rec.tostr", key); lowerer.valueHelpers.set(key, helper); const r = varRef("r.0", t, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 565db0cd8d..93bf43e953 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { dateJsonType, isDateJsonSource } from "../builtins/dates.js"; import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js"; import { pathModuleValue } from "../lower-builtin-values.js"; @@ -406,7 +407,7 @@ export function lowerDynObjectLiteral( if (!(err instanceof PoisonError) || !pureMember) throw err; const fence = lowerer.deferToRuntimeFence(propDiagsBefore, prop, { kind: "closure", - name: () => `%fn${lowerer.lambdaCounter++}_dynfence`, + name: () => lowerer.closureName(loc, "_dynfence"), returnType: VOID, type: { kind: "func", params: [], ret: VOID }, }); @@ -461,7 +462,7 @@ export function lowerDynObjectLiteral( if (!(err instanceof PoisonError)) throw err; const fence = lowerer.deferToRuntimeFence(convDiagsBefore, prop, { kind: "closure", - name: () => `%fn${lowerer.lambdaCounter++}_dynfence`, + name: () => lowerer.closureName(loc, "_dynfence"), returnType: VOID, type: { kind: "func", params: [], ret: VOID }, }); @@ -2956,7 +2957,7 @@ function lowerDeclaredSpreadMerge( const key = `declmerge-source:${type.shapeId}:${source.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%rec.declmerge.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%rec.declmerge", key); const sourceType = source.value.type; const destination = varRef("out.0", type, loc); const receiver = varRef("source.0", sourceType, loc); diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index fbcecdc328..4b8c5692a4 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -240,7 +240,7 @@ export function lowerOptionalChain( const receiver = lowerer.runtimeOptionalSourceValue(recvNode, loweredReceiver) ?? loweredReceiver; if (receiver.type.kind === "dyn") { if (dotNode === expr && ts.isCallExpression(expr)) { - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const callee: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; const stmts: IrStmt[] = []; let guarded = receiver; @@ -298,7 +298,7 @@ export function lowerOptionalChain( ); } if (dotNode === expr && ts.isElementAccessExpression(expr)) { - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const reference: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; lowerer.chainRecvByNode.set(recvNode, reference); lowerer.chainHandled.add(dotNode); @@ -331,7 +331,7 @@ export function lowerOptionalChain( dotNode === expr.expression ) { const m = dotNode.name.text; - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const recvRef: IrExpr = { kind: "chainRecv", id, type: DYN, loc: locOf(recvNode) }; lowerer.chainRecvByNode.set(recvNode, recvRef); lowerer.chainHandled.add(dotNode); @@ -358,7 +358,7 @@ export function lowerOptionalChain( } return { kind: "optChain", id, receiver, body, type: DYN, loc }; } - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const reference: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; lowerer.chainRecvByNode.set(recvNode, reference); lowerer.chainHandled.add(dotNode); @@ -420,7 +420,7 @@ export function lowerOptionalChain( rest.length === 1 ? rest[0]! : { kind: "union" as const, unionId: lowerer.unions.transform(def, rest) }; - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); // A single present arm binds its payload. Multiple present arms bind // the original tagged value, then retag inside the guarded body. The // nullish arms never enter that body, and each record keeps its identity. diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index f0b980da05..6c8dfa3bf0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1,4 +1,5 @@ /* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */ +import { keyedHelperName } from "../stable-names.js"; import { staticallySelected } from "../static-typeof.js"; import * as ts from "../../ts7/adapter.js"; import { lowerHistogramProperty } from "../builtins/performance.js"; @@ -2783,7 +2784,7 @@ export function lowerGroupsProjection( const key = `regexgroups:${shapeId}:${groups.map((g) => `${g.name}=${g.index}`).join(",")}:${recvKey}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%regex.groups.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%regex.groups", key); lowerer.valueHelpers.set(key, helper); const body: IrStmt[] = []; const locals: IrLocal[] = [{ id: "m.0", name: "m", type: recv.type, mutable: true }]; diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 088b45cffa..8363918352 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -1,4 +1,5 @@ /* instanceof and in, including private-name and runtime-key in. */ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { builtinClassInstanceOfAny, builtinClassKind } from "../builtins/builtin-class-dyn.js"; import { @@ -647,7 +648,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const key = `instanceof.union:${unionId}:${target.def.name}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%instanceof.union.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%instanceof.union", key); lowerer.valueHelpers.set(key, helper); const value: IrExpr = { kind: "varRef", localId: "value.0", type: left.type, loc }; const body: IrStmt[] = []; @@ -1847,7 +1848,7 @@ function lowerRuntimeKeyIn( const hkey = `haskey:${recvT.shapeId}`; let helper = lowerer.valueHelpers.get(hkey); if (!helper) { - helper = `%rec.haskey.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%rec.haskey", hkey); lowerer.valueHelpers.set(hkey, helper); const recT: IrType = { kind: "record", shapeId: recvT.shapeId }; diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index abcd5a584a..daf082db73 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -1,4 +1,6 @@ import { staticNameBindings } from "./static-function-names.js"; +import { positionTag } from "./structural-positions.js"; +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import type { Lowerer } from "./lowerer.js"; @@ -229,7 +231,10 @@ export function collectGenericSignature(lowerer: Lowerer, decl: ts.FunctionDecla lowerer.genericFnsBySymbol.set(symbol, { decl, baseName: nameText, - qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText), + qualifiedName: lowerer.qualify( + ts.getSourceFileOfNode(decl), + nsPathPrefix(lowerer, decl) + nameText, + ), typeParams, instances: new Map(), }); @@ -651,7 +656,7 @@ function internGenericInstance( const typeArgsText = `<${rendered.length > 80 ? rendered.slice(0, 77) + "..." : rendered}>`; inst = { path, - name: `${info.qualifiedName}%${info.instances.size}`, + name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""), ordinal: info.instances.size, params, returnType, @@ -2129,7 +2134,7 @@ function internImplicitInstance( const declared = dynamicBroadPromise ? null : implicitDeclaredReturn(lowerer, info); inst = { path, - name: `${info.qualifiedName}%${info.instances.size}`, + name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""), ordinal: info.instances.size, params: shapes, // The promise callers rely on before the body settles it: the @@ -2317,7 +2322,7 @@ export function implicitLocalFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(stmt ?? decl, decl) + `${name}%l${ts.tokenStartOfNode(decl)}`, + nsPathPrefix(lowerer, stmt ?? decl, decl) + `${name}%l${positionTag(lowerer, decl)}`, ), typeParams: [], instances: new Map(), @@ -2413,7 +2418,7 @@ export function objLitGenericFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(fnNode), - `%ol${ts.tokenStartOfNode(fnNode)}.${name}`, + `%ol${positionTag(lowerer, fnNode)}.${name}`, ), typeParams, instances: new Map(), @@ -2559,7 +2564,7 @@ export function bindingGenericFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(stmt ?? decl, decl) + + nsPathPrefix(lowerer, stmt ?? decl, decl) + (topLevel ? name : `${name}%l${ts.tokenStartOfNode(decl)}`), ), typeParams, diff --git a/packages/compiler/src/frontend/lowering/intern-tables.ts b/packages/compiler/src/frontend/lowering/intern-tables.ts index fc69237db1..e3f2b08890 100644 --- a/packages/compiler/src/frontend/lowering/intern-tables.ts +++ b/packages/compiler/src/frontend/lowering/intern-tables.ts @@ -23,6 +23,7 @@ * The entry handles that itable.find and itable.insert answer stay inside * these lowerings: f64 locals that only reach toBool and itable.value. * Any other use of the class or its members as values is refused. */ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { BOOL, F64, STRING, funcOf, typeKey } from "../../ir/ir.js"; import type { IrExpr, IrFunction, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; @@ -447,7 +448,11 @@ function helperName(lowerer: Lowerer, member: InternTableMember, signature: stri } function registerHelper(lowerer: Lowerer, member: InternTableMember, signature: string): string { - const name = `%${INTERN_TABLE_CLASS}.${member}.${lowerer.internTableHelpers.size}`; + const name = keyedHelperName( + lowerer, + `%${INTERN_TABLE_CLASS}.${member}`, + `${member}|${signature}`, + ); lowerer.internTableHelpers.set(`${member}|${signature}`, name); return name; } diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index 898c46f205..e50652182e 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { BOOL, @@ -86,7 +87,7 @@ function dynIteratorAdapter( const key = `iterAdapt:${typeKey(expected)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%iter.adapt.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%iter.adapt", key); lowerer.valueHelpers.set(key, name); const locals: IrLocal[] = []; const local = (id: string, type: IrType, mutable = false): IrExpr => { @@ -313,7 +314,7 @@ export function lowerCollectionIteratorValue( loc, }; if (!typeEquals(yielded.type, type.yieldT)) return null; - name = `%coll.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%coll.iter", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -411,7 +412,7 @@ export function lowerBytesIteratorValue( const key = `bytesIter:${method}:${typeKey(bytesT)}:${typeKey(type)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%bytes.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%bytes.iter", key); lowerer.valueHelpers.set(key, name); const list = varRef("list.0", bytesT, loc); const index = varRef("index.0", F64, loc); @@ -517,7 +518,7 @@ export function lowerStringIteratorValue( const key = `strIter:${typeKey(type)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%str.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%str.iter", key); lowerer.valueHelpers.set(key, name); const text = varRef("text.0", STRING, loc); const index = varRef("index.0", F64, loc); @@ -677,7 +678,7 @@ function generatorRetypeAdapter( type: resumeType(expected.nextT), loc, }; - name = `%gen.retype.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%gen.retype", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -794,7 +795,7 @@ export function generatorDrain( const outRef = varRef("out.0", arrT, loc); const value = lowerer.coerceToExpected(genValue(genRef, loc), elem); if (!typeEquals(value.type, elem)) return null; - name = `%gen.drain.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%gen.drain", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -856,7 +857,7 @@ export function generatorDrain( if (!raw) return null; const value = lowerer.coerceToExpected(raw, elem); if (!typeEquals(value.type, elem)) return null; - name = `%gen.drain.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%gen.drain", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -978,7 +979,7 @@ export function lowerArrayIteratorValue( }; } if (!yielded || !typeEquals(yielded.type, type.yieldT)) return null; - name = `%arr.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.iter", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts index b9bbf62168..e27dd41837 100644 --- a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts +++ b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts @@ -21,6 +21,7 @@ * files only, and an empty answer leaves every mapping unchanged, so * TypeScript programs and JavaScript that never uses these patterns * compile exactly as before. */ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { isJsSourceFile, locOf } from "../program.js"; import { BOOL, DYN, type IrExpr, type IrStmt } from "../../ir/ir.js"; @@ -215,10 +216,10 @@ export function lowerValueReturningConstructorNew( "spread arguments when constructing a class whose constructor returns a value", ); } - const key = `ctorValue:${ts.getSourceFileOfNode(decl).fileName}:${ts.tokenStartOfNode(decl)}`; + const key = `ctorValue:${lowerer.positions.of(decl)}`; let name = lowerer.valueHelpers.get(key); if (name === undefined) { - name = `%ctorValue.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%ctorValue", key); lowerer.valueHelpers.set(key, name); const savedClass = lowerer.currentClass; lowerer.currentClass = null; diff --git a/packages/compiler/src/frontend/lowering/json-require.ts b/packages/compiler/src/frontend/lowering/json-require.ts index 9277452537..d6c2da571d 100644 --- a/packages/compiler/src/frontend/lowering/json-require.ts +++ b/packages/compiler/src/frontend/lowering/json-require.ts @@ -10,6 +10,7 @@ * JSON module is ordinary storage holding that object — destructuring * copies its properties at the require, exactly like Node. ES-module JSON * imports keep their own record (Node's ESM loader caches separately). */ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { PoisonError } from "./lowerer.js"; @@ -84,7 +85,7 @@ export function collectJsonRequires(lowerer: Lowerer, parts: FileParts[]): void ); throw new PoisonError(); } - const id = `%g.jsonRequire.${lowerer.jsonRequireModules.size}`; + const id = keyedHelperName(lowerer, "%g.jsonRequire", jsonSf.fileName); const global: IrGlobal = { id, name: "%json", type: mapped, mutable: true }; const loadedId = `${id}%loaded`; lowerer.globalsList.push(global, { diff --git a/packages/compiler/src/frontend/lowering/lifetime-regions.ts b/packages/compiler/src/frontend/lowering/lifetime-regions.ts index b212ae473f..b684c0395b 100644 --- a/packages/compiler/src/frontend/lowering/lifetime-regions.ts +++ b/packages/compiler/src/frontend/lowering/lifetime-regions.ts @@ -14,6 +14,7 @@ * Any other use of the class or its methods as values is refused. The * module is marked lifetimeRegions, which moves every ProgramLifetime * class into counted regions. */ +import { keyedHelperName } from "./stable-names.js"; import { F64, VOID, funcOf, typeKey } from "../../ir/ir.js"; import type { IrExpr, IrFunction, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js"; @@ -152,7 +153,7 @@ function runHelper(lowerer: Lowerer, fnType: IrType & { kind: "func" }, loc: Src const key = typeKey(fnType); const known = lowerer.regionRunHelpers.get(key); if (known !== undefined) return known; - const name = `%${LIFETIME_REGION_CLASS}.run.${lowerer.regionRunHelpers.size}`; + const name = keyedHelperName(lowerer, `%${LIFETIME_REGION_CLASS}.run`, key); lowerer.regionRunHelpers.set(key, name); const regionT = lifetimeRegionType(lowerer); const ret = fnType.ret; diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index 460f4d53a7..bc51a93826 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { InternalCompilerError } from "../../errors.js"; /* The node:assert lowering (a spoke module like lower-dgram.ts): the * callable module form (`assert(x)` through a default import or a CJS @@ -667,7 +668,7 @@ function partialEqHelper(lowerer: Lowerer, ta: IrType, tb: IrType, loc: SrcLoc): const key = `assert.pdeq:${typeKey(ta)}|${typeKey(tb)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.pdeq.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, "%assert.pdeq", key); lowerer.assertHelpers.set(key, name); const a = (): IrExpr => varRef("a.0", ta, loc); const b = (): IrExpr => varRef("b.0", tb, loc); @@ -1280,7 +1281,7 @@ function deepCheckCall( const key = `assert.dcheck:${variant}:${negated}:${typeKey(a.type)}:${typeKey(b.type)}`; let name = lowerer.assertHelpers.get(key); if (!name) { - name = `%assert.dcheck.${lowerer.assertHelpers.size}`; + name = keyedHelperName(lowerer, "%assert.dcheck", key); lowerer.assertHelpers.set(key, name); const pa = varRef("a.0", a.type, loc); const pb = varRef("b.0", b.type, loc); @@ -1844,7 +1845,7 @@ function assertThrowsHelper( const key = `assert.${mode}:${recvIsPromise ? "p" : "f"}:${typeKey(recvType)}:${sig}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.${mode}.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, `%assert.${mode}`, key); lowerer.assertHelpers.set(key, name); const caughtRef = (): IrExpr => varRef("e.0", CAUGHT, loc); @@ -2293,7 +2294,7 @@ function ifErrorHelper(lowerer: Lowerer, t: IrType & { kind: "union" }, loc: Src const key = `assert.iferr:${typeKey(t)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.iferr.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, "%assert.iferr", key); lowerer.assertHelpers.set(key, name); const def = lowerer.unions.get(t.unionId); if (!def) throw new InternalCompilerError(`assert.ifError over unknown union ${t.unionId}`); @@ -2464,7 +2465,7 @@ function deepEqHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc, loose = false): const key = `assert.${prefix}:${typeKey(t)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.${prefix}.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, `%assert.${prefix}`, key); lowerer.assertHelpers.set(key, name); const a = (): IrExpr => varRef("a.0", t, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 7102f37ab1..63b8781f2b 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; import { lowerDynStreamMemberCall } from "./dyn-streams.js"; @@ -2094,7 +2095,7 @@ function lowerOptionalNumberDefault( const key = `number.optionalDefault:${widened.unionId}:${dflt}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%number.optionalDefault.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%number.optionalDefault", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", widened, loc); lowerer.liftedFns.push({ @@ -2203,7 +2204,7 @@ function primitiveConversionHandler( const key = `then.${name}:${typeKey(inner)}`; let fnName = lowerer.arrHofHelpers.get(key); if (fnName === undefined) { - fnName = `%then.${name}.${lowerer.arrHofHelpers.size}`; + fnName = keyedHelperName(lowerer, `%then.${name}`, key); lowerer.arrHofHelpers.set(key, fnName); const value = varRef("v.0", inner, loc); let body: IrExpr; @@ -2372,7 +2373,7 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc) const key = `bigint.scalar:${unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%bigint.scalar.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%bigint.scalar", key); lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); const body: IrStmt[] = []; @@ -3234,7 +3235,7 @@ function lowerScalarUnionNumber( const key = `number.scalar:${arg.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%number.scalar.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%number.scalar", key); lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); const nan: IrExpr = { @@ -3279,7 +3280,7 @@ function lowerOptionalStringNumber( const key = `${label}.optionalString:${arg.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%${label}.optionalString.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%${label}.optionalString`, key); lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); lowerer.liftedFns.push({ @@ -4706,7 +4707,7 @@ function lowerDefaultToStringCall( const key = `objToStr:${typeKey(recv.type)}${missing ? `:${missing}` : ""}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%obj.tostr.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.tostr", key); lowerer.valueHelpers.set(key, helper); const o = varRef("o.0", recv.type, loc); lowerer.liftedFns.push({ @@ -5393,7 +5394,7 @@ function loweredTemplateStrings( const site = lowerer.globalsList.length; const globalId = `%g.templateStrings.${site}`; const readyId = `%g.templateStringsReady.${site}`; - getter = `%templateStrings.get.${lowerer.valueHelpers.size}`; + getter = keyedHelperName(lowerer, "%templateStrings.get", key); lowerer.valueHelpers.set(key, getter); lowerer.globalsList.push( { id: globalId, name: "%templateStrings", type, mutable: true }, @@ -5574,7 +5575,7 @@ export function lowerPromiseMethodCall( // IS the spec's identity/thrower/no-op substitution for all three // members. An argument expression, when present, is undefined/null // by construction — nothing to evaluate. - const fnName = `%fn${lowerer.lambdaCounter++}_${member}pass`; + const fnName = lowerer.closureName(loc, `_${member}pass`); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT], ret: promT }; const fnCtx = newFnCtx(true, null, funcType, inner); fnCtx.isAsync = true; @@ -5671,7 +5672,7 @@ export function lowerPromiseMethodCall( // Evaluate both callback expressions before the wrapper runs, in source order. const fulfilled = fulfilledAbsent ? null : boxHandler(fulfilledNode, true); const rejected = rejectedAbsent ? null : boxHandler(rejectedNode, false); - const fnName = `%fn${lowerer.lambdaCounter++}_then2`; + const fnName = lowerer.closureName(loc, "_then2"); const paramTypes: IrType[] = [ promT, ...(fulfilled ? [fulfilled.value.type] : []), @@ -5920,7 +5921,7 @@ export function lowerPromiseMethodCall( ); }; const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: DYN }; - const fnName = `%fn${lowerer.lambdaCounter++}_then`; + const fnName = lowerer.closureName(loc, "_then"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, DYN], @@ -6015,7 +6016,7 @@ export function lowerPromiseMethodCall( ); } const R = resultT.inner; - const fnName = `%fn${lowerer.lambdaCounter++}_then`; + const fnName = lowerer.closureName(loc, "_then"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, cb.type], @@ -6118,7 +6119,7 @@ export function lowerPromiseMethodCall( "finally callbacks with parameters or a return value (use () => { ... })", ); } - const fnName = `%fn${lowerer.lambdaCounter++}_finally`; + const fnName = lowerer.closureName(loc, "_finally"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, cb.type], @@ -6221,7 +6222,7 @@ export function lowerPromiseMethodCall( } if (cb.type.kind === "dyn") { const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: DYN }; - const fnName = `%fn${lowerer.lambdaCounter++}_catchdyn`; + const fnName = lowerer.closureName(loc, "_catchdyn"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, DYN], @@ -6351,7 +6352,7 @@ export function lowerPromiseMethodCall( ); } const R = resultT.inner; - const fnName = `%fn${lowerer.lambdaCounter++}_catch`; + const fnName = lowerer.closureName(loc, "_catch"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT], ret: resultT }; const fnCtx = newFnCtx(true, null, funcType, R); fnCtx.isAsync = true; @@ -6588,7 +6589,7 @@ function filterNarrowHelper( const key = `filterNarrow:${test}:${typeKey(elem)}:${typeKey(outElem)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.filterNarrow.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.filterNarrow", key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); @@ -6834,7 +6835,7 @@ export function recordKeysArrayCall( const key = `obj.keys:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.keys.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.keys", key); const ref: IrExpr = { kind: "varRef", localId: "r.0", type: argIr, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: resultT, loc }; lowerer.arrHofHelpers.set(key, helper); @@ -6904,7 +6905,7 @@ function recordHasOwnHelper(lowerer: Lowerer, shapeId: string, loc: SrcLoc): str const key = `obj.hasOwn:${shapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = `%obj.hasOwn.${lowerer.arrHofHelpers.size}`; + const helper = keyedHelperName(lowerer, "%obj.hasOwn", key); lowerer.arrHofHelpers.set(key, helper); const shape = lowerer.shapes.get(shapeId)!; const recT: IrType = { kind: "record", shapeId }; @@ -6989,7 +6990,7 @@ function classAssignHelper( const key = `obj.assignClass:${className}:${srcShapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = `%obj.assignClass.${lowerer.arrHofHelpers.size}`; + const helper = keyedHelperName(lowerer, "%obj.assignClass", key); lowerer.arrHofHelpers.set(key, helper); const sShape = lowerer.shapes.get(srcShapeId)!; const oT: IrType = { kind: "object", className }; @@ -7057,7 +7058,7 @@ function recordAssignHelper( const key = `obj.assign:${targetShapeId}:${srcShapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = `%obj.assign.${lowerer.arrHofHelpers.size}`; + const helper = keyedHelperName(lowerer, "%obj.assign", key); lowerer.arrHofHelpers.set(key, helper); const sShape = lowerer.shapes.get(srcShapeId)!; const tShape = lowerer.shapes.get(targetShapeId)!; @@ -8514,7 +8515,7 @@ function lowerObjectStaticCall( const key = `obj.${member}:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.${member}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%obj.${member}`, key); const recT = argIr; const ref: IrExpr = { kind: "varRef", localId: "r.0", type: recT, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: resultT, loc }; @@ -9150,7 +9151,7 @@ function lowerInlineArrowBind( const length = firstDefault < 0 ? arrow.parameters.nodes.length : firstDefault; const key = `arrow.bind:${typeKey(type)}:${length}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%arrow.bind.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%arrow.bind", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); @@ -10079,7 +10080,7 @@ function lowerUnionObjectDynFieldCall( const receiverRef = varRef(receiverLocal.id, receiverT, loc); let helper = lowerer.unionCallHelpers.get(key); if (!helper) { - helper = `%union.call.${lowerer.unionCallHelpers.size}`; + helper = keyedHelperName(lowerer, "%union.call", key); lowerer.unionCallHelpers.set(key, helper); const receiverRef = varRef("this.0", receiverT, loc); const branch = (plan: (typeof plans)[number]): IrStmt[] => { @@ -10172,7 +10173,7 @@ function unionObjectMethodHelper( const key = `${receiverT.unionId}:${method}:${shapes.map((shape) => `${shape.mode}:${typeKey(shape.type)}`).join(",")}:${typeKey(resultT)}`; const existing = lowerer.unionCallHelpers.get(key); if (existing) return existing; - const name = `%union.call.${lowerer.unionCallHelpers.size}`; + const name = keyedHelperName(lowerer, "%union.call", key); lowerer.unionCallHelpers.set(key, name); // A probe that catches a refusal while the body lowers must not leave // the name registered for a helper that was never emitted. @@ -10366,7 +10367,7 @@ export function lowerStaticNumberGlobal( const key = `parseInt.optional:${unionT.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%parseInt.optional.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%parseInt.optional", key); lowerer.valueHelpers.set(key, helper); const value: IrExpr = { kind: "varRef", localId: "value.0", type: unionT, loc }; const radixRef: IrExpr = { kind: "varRef", localId: "radix.0", type: F64, loc }; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 790efe988d..fcea03790e 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; import { adaptPromiseExecutor } from "./promise-executor-adapter.js"; import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; @@ -4663,7 +4664,7 @@ export function genericClassInstanceType( ); } const ordinal = gci.instances.size; - const name = `${gci.family.def.name}%${ordinal}`; + const name = keyedHelperName(lowerer, `${gci.family.def.name}%`, key, ""); const entry: { name: string; info: ClassInfo | null; poisoned?: boolean } = { name, info: null }; gci.instances.set(key, entry); const bindings = new Map(); @@ -7910,7 +7911,7 @@ export function classIteratorDrainCall( const key = `${cit.className}:${typeKey(outElem)}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { - name = `%iter.drain.${lowerer.iterDrainHelpers.size}`; + name = keyedHelperName(lowerer, "%iter.drain", key); lowerer.iterDrainHelpers.set(key, name); const recvT: IrType = { kind: "object", className: cit.className }; const recvRef: IrExpr = { kind: "varRef", localId: "r.0", type: recvT, loc }; @@ -8024,7 +8025,7 @@ export function classIteratorRestDrainCall( const key = `it:${cit.iterT.className}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { - name = `%iter.drain.${lowerer.iterDrainHelpers.size}`; + name = keyedHelperName(lowerer, "%iter.drain", key); lowerer.iterDrainHelpers.set(key, name); const itRef: IrExpr = { kind: "varRef", localId: "it.0", type: cit.iterT, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: outT, loc }; @@ -10532,7 +10533,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const key = `x509.record:${isBytes ? "bytes" : "str"}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%x509.record.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%x509.record", key); lowerer.valueHelpers.set(key, helper); const dataT = data.type; const dRef: IrExpr = { kind: "varRef", localId: "d.0", type: dataT, loc }; diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index 5c99ebc821..aa90fc7c74 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, boolLit } from "../../ir/build.js"; /* The dgram/dns-surface lowering (node:dgram + node:dns — a spoke module * like lower-server.ts): module-function calls (dgram createSocket, dns @@ -357,7 +358,7 @@ function checkedDynPromise( const key = `dns.promise:${typeKey(declared)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%dns.promise.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%dns.promise", key); lowerer.valueHelpers.set(key, helper); const promiseT: IrType = { kind: "promise", inner: DYN }; lowerer.liftedFns.push({ @@ -891,7 +892,7 @@ export function lowerPromisifiedDnsCall( const key = `dnsPromise:${JSON.stringify(value)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%dnsPromise.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%dnsPromise", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index f472672b20..f105af786d 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -42,6 +42,7 @@ * emit-overrides block at the bottom of this file (per-event * monomorphization of the override body, dispatch on the dynamic class, * super.emit as the prototype chain). */ +import { keyedHelperName } from "./stable-names.js"; import { checkKeypressListener } from "./builtins/readline.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; @@ -631,7 +632,7 @@ function lowerFlexListenerCall( const key = `emitter.${registering ? "onFlex" : "offDyn"}:${typeKey(recvT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.${registering ? "onFlex" : "offFlex"}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%emitter.${registering ? "onFlex" : "offFlex"}`, key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -850,7 +851,7 @@ function lowerDynListenerCall( const existing = lowerer.arrHofHelpers.get(key); let helper = existing; if (!helper) { - helper = `%emitter.${registering ? "onDyn" : "offDyn"}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%emitter.${registering ? "onDyn" : "offDyn"}`, key); lowerer.arrHofHelpers.set(key, helper); const check: IrStmt = { kind: "exprStmt", @@ -1000,7 +1001,7 @@ export function lowerDynamicEmitterEmit( const key = `emitter.dynEmit:${typeKey(recvT)}:${k}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.dynEmit.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%emitter.dynEmit", key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -1092,7 +1093,7 @@ export function lowerDynamicEmitterOn( const key = `emitter.dynOn:${typeKey(recvT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.dynOn.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%emitter.dynOn", key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -2359,7 +2360,7 @@ function emitDispatchExpr( const key = `emitter.emitDispatch:${typeKey(recvT)}:${name}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.emitDispatch.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%emitter.emitDispatch", key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -2424,7 +2425,7 @@ function eventsOnceBoxer( const key = `eventsOnce:${typeKey(fnType)}`; let name = lowerer.valueHelpers.get(key); if (name === undefined) { - name = `%events.onceArgs.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%events.onceArgs", key); lowerer.valueHelpers.set(key, name); const params: IrParam[] = tuple.map((type, i) => ({ localId: `%once.a${i}`, diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index e7a1b79a83..ba96bea8ca 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; @@ -2274,7 +2275,11 @@ export function cjsAccessorSnapshotOf( if (!lexerVisibleAccessors(ts.getSourceFileOfNode(getter)).includes(getter)) return null; const call = cjsAccessorCall(lowerer, getter, locOf(getter)); if (call === null) return null; - const snapshot = { id: `%g.cjsSnapshot.${lowerer.cjsAccessorSnapshots.size}`, type: call.type }; + const at = locOf(getter); + const snapshot = { + id: keyedHelperName(lowerer, "%g.cjsSnapshot", `${at.file}:${at.start}:${at.end}`), + type: call.type, + }; lowerer.globalsList.push({ id: snapshot.id, name: "%cjsSnapshot", @@ -2299,7 +2304,7 @@ export function cjsAccessorSnapshotStores( if (getters.length === 0) return []; let readyId = lowerer.cjsSnapshotReady.get(dep); if (readyId === undefined) { - readyId = `%g.cjsSnapshotReady.${lowerer.cjsSnapshotReady.size}`; + readyId = keyedHelperName(lowerer, "%g.cjsSnapshotReady", dep.fileName); lowerer.globalsList.push({ id: readyId, name: "%cjsSnapshotReady", type: BOOL, mutable: true }); lowerer.cjsSnapshotReady.set(dep, readyId); } diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index cd8dc31d1d..653c0d837e 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -270,7 +270,7 @@ export function lowerLambda( const nameIdent = !ts.isArrowFunction(node) && node.name && ts.isIdentifier(node.name) ? node.name : null; const baseName = nameIdent ? nameIdent.text : ""; - const fnName = `%fn${lowerer.lambdaCounter++}${baseName ? `_${baseName}` : ""}`; + const fnName = lowerer.closureName(locOf(node), baseName ? `_${baseName}` : ""); // Named function expressions/declarations can self-reference by name; an // object-literal method's name is a PROPERTY, not a binding — no self. const selfSymbol = diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 4be5d0b7ef..1979b43aa8 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, boolLit, @@ -268,7 +269,7 @@ function errorInspectHelper( const key = `insp.userError:${typeKey(t)}`; const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; - const name = `%util.insp.${lowerer.inspectHelpers.size}`; + const name = keyedHelperName(lowerer, "%util.insp", key); lowerer.inspectHelpers.set(key, name); const v = (): IrExpr => varRef("v.0", t, loc); const r = (): IrExpr => varRef("r.0", F64, loc); @@ -1145,7 +1146,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { const key = `insp:${typeKey(t)}`; const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; - const name = `%util.insp.${lowerer.inspectHelpers.size}`; + const name = keyedHelperName(lowerer, "%util.insp", key); lowerer.inspectHelpers.set(key, name); const v = (): IrExpr => varRef("v.0", t, loc); @@ -2074,7 +2075,7 @@ function formatUnionHelper( const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; const loc = helperLoc(site); - const name = `%util.fmtv.${lowerer.inspectHelpers.size}`; + const name = keyedHelperName(lowerer, "%util.fmtv", key); lowerer.inspectHelpers.set(key, name); const def = lowerer.unions.get(t.unionId); if (!def) throw new InternalCompilerError(`format value over unknown union ${t.unionId}`); diff --git a/packages/compiler/src/frontend/lowering/lower-mixins.ts b/packages/compiler/src/frontend/lowering/lower-mixins.ts index 766059dfb9..9de027563f 100644 --- a/packages/compiler/src/frontend/lowering/lower-mixins.ts +++ b/packages/compiler/src/frontend/lowering/lower-mixins.ts @@ -34,6 +34,7 @@ * statics-bearing mixin classes outside a plain top-level statement * (their declaration-time code must run exactly where the call * evaluates). */ +import { positionTag } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { PoisonError } from "./lowerer.js"; @@ -452,7 +453,7 @@ function instantiateMixinCall( const base = mixinBaseClassOf(lowerer, call.arguments.nodes[0]!); const instName = lowerer.qualify( ts.getSourceFileOfNode(call), - `%mx${ts.tokenStartOfNode(call)}.${shape.classNode.name?.text ?? ""}`, + `%mx${positionTag(lowerer, call)}.${shape.classNode.name?.text ?? ""}`, ); const ordinal = lowerer.mixinOrdinals.get(shape.classNode) ?? 0; lowerer.mixinOrdinals.set(shape.classNode, ordinal + 1); diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts index fcc68c787a..8c50dee460 100644 --- a/packages/compiler/src/frontend/lowering/lower-mock.ts +++ b/packages/compiler/src/frontend/lowering/lower-mock.ts @@ -17,6 +17,7 @@ * writes the original back on restore(). Each mock registers a closure * that calls its restore() with its tracker (scr_test.c): the module's * `mock`, or the test's t.mock, which is reset when the test ends. */ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { BOOL, @@ -136,7 +137,7 @@ function liftOnce( const key = `mock.${role}:${owner}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%mock.${role}.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, `%mock.${role}`, key); lowerer.assertHelpers.set(key, name); build(name); return name; diff --git a/packages/compiler/src/frontend/lowering/lower-modules.test.ts b/packages/compiler/src/frontend/lowering/lower-modules.test.ts index 84cc96e7dd..f1021e4da8 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.test.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.test.ts @@ -34,7 +34,7 @@ test("retains dependencies from function storage, captures, generator channels a const completed = record("completed"); const body = record("body"); const global = record("global", record("nested")); - const wanted = lowerer.shapes.shapes.map((shape) => shape.id); + const wanted = lowerer.shapes.shapes.map((shape) => shape.id).sort(); record("unused"); const unionId = lowerer.unions.intern([body, UNDEFINED_T]); const unusedUnion = lowerer.unions.intern([F64, UNDEFINED_T]); @@ -162,7 +162,7 @@ test("closes nested structural types and index-signature values", () => { sent = record("sent"), indexed = record("indexed"); const containerId = lowerer.shapes.intern([], false, indexed); - const wanted = lowerer.shapes.shapes.map((shape) => shape.id); + const wanted = lowerer.shapes.shapes.map((shape) => shape.id).sort(); record("unused"); const nested: IrType = { kind: "func", diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index d1f276d44b..0b99e88d3e 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -1,4 +1,5 @@ import { THREADS_MODULE } from "../threads-import.js"; +import { positionTag } from "./structural-positions.js"; import { EXPERIMENTAL_MODULE } from "../experimental-import.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { everyStmtList, everyTypeChild } from "../../ir/traverse.js"; @@ -1074,8 +1075,14 @@ export function moduleArtifacts( else delete rest.abstractMethods; return rest; }), - records: lowerer.shapes.shapes.filter((r) => shapeIds.has(r.id)), - unions: lowerer.unions.unions.filter((u) => unionIds.has(u.id)), + // Ids come from what each shape and union is (keyedHelperName), so + // ordering by id gives every lowering order the same module. + records: lowerer.shapes.shapes + .filter((r) => shapeIds.has(r.id)) + .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), + unions: lowerer.unions.unions + .filter((u) => unionIds.has(u.id)) + .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), }; } @@ -1677,7 +1684,7 @@ export function collectGlobals( } if (type.kind === "void") lowerer.badType(stmt.name, lowerer.typeOf(stmt.name)); const g: IrGlobal = { - id: `%g.${tag}${nsPathPrefix(stmt)}%alias.${stmt.name.text}%${ts.tokenStartOfNode(stmt)}`, + id: `%g.${tag}${nsPathPrefix(lowerer, stmt)}%alias.${stmt.name.text}%${positionTag(lowerer, stmt)}`, name: stmt.name.text, type, mutable: false, @@ -1754,7 +1761,7 @@ export function collectGlobals( // (`%g.e.A%B.x`), so `namespace A { export const x }` and a // top-level `const x` never collide; non-exported members add the // declaring block's position (block-local bindings). - const nsPrefix = nsPathPrefix(stmt, list.declarations.nodes[0]); + const nsPrefix = nsPathPrefix(lowerer, stmt, list.declarations.nodes[0]); for (const decl of list.declarations.nodes) { // --provenance-sources: an elided pure-annotated dead const in a // fetched source module registers no global (the statement diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index dca835d779..19123dae3d 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -36,6 +36,7 @@ * of the compiler already takes (a body running before the declaring * statement is not modeled — same as top-level const reads through early * calls). */ +import { positionTag, type PositionTagOwner } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import { packageShimFor } from "../package-shims.js"; import type { Lowerer } from "./lowerer.js"; @@ -164,7 +165,11 @@ function registerTypeOnlyBlocks(lowerer: Lowerer, decl: ts.ModuleDeclaration): v * distinct (resolution is by symbol; names only need uniqueness and * determinism). `modifierDecl` carries the declaration whose modifiers * decide exportedness (the first declarator of a variable statement). */ -export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string { +export function nsPathPrefix( + owner: PositionTagOwner, + node: ts.Node, + modifierDecl?: ts.Node, +): string { const parent = node.parent; if (parent === undefined || !ts.isModuleBlock(parent)) return ""; const flagsHost = modifierDecl ?? node; @@ -179,10 +184,10 @@ export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string { const nested = p.parent !== undefined && ts.isModuleBlock(p.parent); const nonExported = nested && (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Export) === 0; - segs.unshift(p.name.text + (nonExported ? `%${ts.tokenStartOfNode(p)}` : "")); + segs.unshift(p.name.text + (nonExported ? `%${positionTag(owner, p)}` : "")); } } - return `${segs.join("%")}${exported ? "" : `%${ts.tokenStartOfNode(parent)}`}.`; + return `${segs.join("%")}${exported ? "" : `%${positionTag(owner, parent)}`}.`; } /** The registered namespace-block kind a symbol's declaration sits in diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 058d7874aa..94d49d8b37 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, nodeThrowExpr, @@ -183,7 +184,7 @@ export function voidizedCallback(lowerer: Lowerer, cb: IrExpr, loc: SrcLoc): IrE const toT = funcOf([...fromT.params], VOID); const key = `server.voidcb:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%server.voidcb.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%server.voidcb", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; @@ -279,7 +280,7 @@ function receiverReturningCall( ): IrExpr { const key = `server.recvret:${fn}:${opts?.prefix?.fn ?? ""}:${(opts?.mainArgIndices ?? []).join(".")}:${callArgs.map((a) => typeKey(a.type)).join(",")}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%server.recvret.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%server.recvret", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const params = callArgs.map((a, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: a.type })); @@ -590,7 +591,7 @@ function h2HeadersCallbackAdapter( ); const key = `h2.hdrcb:${handleKind}:${withStatus}:${typeKey(fromT)}:${shapeId ?? ""}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%h2.hdrcb.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%h2.hdrcb", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; @@ -3583,7 +3584,7 @@ export function lowerCompatReqStreamOptionalCall( type: unionT, loc, }; - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const body = lowerH2StreamMethodCall(lowerer, call, call.expression, { kind: "chainRecv", id, @@ -4424,7 +4425,7 @@ function lowerHttpCreateServerForms( const hasMax = opts.maxHeaderSize !== null; const key = `server.opts:${opts.joinDup ? 1 : 0}:${opts.allowMissingHost ? 1 : 0}:${hasBuffer ? 1 : 0}:${hasMax ? 1 : 0}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%server.opts.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%server.opts", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const params = [ @@ -8009,7 +8010,7 @@ function lowerHttpResMethodCall( const all = [...callArgs, cb]; const key = `server.endcb:${fn}:${all.map((a) => typeKey(a.type)).join(",")}`; const existing = lowerer.arrHofHelpers.get(key); - const helper = existing ?? `%server.endcb.${lowerer.arrHofHelpers.size}`; + const helper = existing ?? keyedHelperName(lowerer, "%server.endcb", key); if (!existing) { lowerer.arrHofHelpers.set(key, helper); const params = all.map((a, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: a.type })); diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 2ad3657c7b..0beb24a0ea 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -2278,7 +2278,7 @@ function lowerStaticNamespaceImport( ): IrExpr { const loc = locOf(call); const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: ns }; - const fnName = `%fn${lowerer.lambdaCounter++}_staticImport`; + const fnName = lowerer.closureName(loc, "_staticImport"); const fnType: IrType & { kind: "func" } = { kind: "func", params: [], ret: resultT }; const fnCtx = newFnCtx(true, null, fnType, ns); fnCtx.isAsync = true; diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index b9add95a61..a75ec43b72 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { nodeThrowExpr, boolLit, numLit, strLit } from "../../ir/build.js"; import { hashStreamOf, isHashValueType } from "./builtins/hash-stream.js"; import { InternalCompilerError } from "../../errors.js"; @@ -311,7 +312,7 @@ function lowerStreamCallback( ); } } - const fnName = `%fn${lowerer.lambdaCounter++}_${which}cb`; + const fnName = lowerer.closureName(loc, `_${which}cb`); const fnCtx = newFnCtx(true, null, funcType, funcType.ret); lowerer.fnStack.push(fnCtx); try { @@ -1144,7 +1145,7 @@ function streamMethodWrapper( paramTypes.push(got.type); } const ret = found.sig.ret; - const fnName = `%fn${lowerer.lambdaCounter++}_${which}vt`; + const fnName = lowerer.closureName(loc, `_${which}vt`); const funcType = funcOf([thisType, ...paramTypes], ret); lowerer.fnStack.push(newFnCtx(true, null, funcType, ret)); try { @@ -1690,7 +1691,7 @@ function fromGenHelpers( const key = `streamFromGen:${role}:${typeKey(gen)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.fromGen.${role}.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, `%stream.fromGen.${role}`, key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -1779,7 +1780,7 @@ function classIteratorObject( const key = `streamFromIter:${cit.iterT.className}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.fromIter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.fromIter", key); lowerer.valueHelpers.set(key, name); const nextName = `${name}.next`; const nextT = funcOf([], DYN); @@ -1940,7 +1941,7 @@ function classAsyncIteratorObject( const key = `streamFromAsyncIter:${cit.className}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.fromAsyncIter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.fromAsyncIter", key); lowerer.valueHelpers.set(key, name); const it: IrLocal = { id: "it.0", name: "it", type: cit.iterT, mutable: false, boxed: true }; const src: IrLocal = { @@ -2894,7 +2895,7 @@ function pipelineBoxHelper( const key = `pipelineBox:${typeKey(promiseT)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.pipelineBox.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.pipelineBox", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -2921,7 +2922,7 @@ function pipelineAbortHelper(lowerer: Lowerer, ac: IrExpr, loc: SrcLoc): IrExpr let name = lowerer.valueHelpers.get(key); const abortT = funcOf([], VOID); if (!name) { - name = `%stream.pipelineAbort.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.pipelineAbort", key); lowerer.valueHelpers.set(key, name); const acLocal: IrLocal = { id: "ac.0", name: "ac", type: DYN, mutable: false, boxed: true }; const acRef = varRef(acLocal.id, DYN, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index 7d4bba34b6..03c9c5c1f2 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; import { isJsSourceFile } from "../program.js"; @@ -55,7 +56,7 @@ export function adaptZeroArgTimerCallback( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.dropret:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%timer.dropret.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%timer.dropret", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( @@ -117,7 +118,7 @@ export function adaptZeroArgTimerCallback( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.droparg:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%timer.droparg.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%timer.droparg", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( @@ -203,7 +204,7 @@ function makeTimerArgsThunk( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.argsthunk:${n}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%timer.argsthunk.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%timer.argsthunk", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; diff --git a/packages/compiler/src/frontend/lowering/lowerer.test.ts b/packages/compiler/src/frontend/lowering/lowerer.test.ts index bacdc7941c..750c27694c 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.test.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.test.ts @@ -10,6 +10,7 @@ import { } from "../../ir/ir.js"; import type { Node, Program, SourceFile, Symbol, Type } from "../ts7/adapter.js"; import { Lowerer } from "./lowerer.js"; +import { UnionRegistry } from "../type-mapper.js"; import { classMethodValue } from "./class-method-values.js"; const loc = { file: "island.ts", start: 0, end: 1 }; @@ -32,7 +33,7 @@ test("reuses complete copy routes without repeating discriminator planning", () expect(plan).not.toHaveBeenCalled(); expect(lowerer.liftedFns.filter((fn) => fn.name === helper)).toHaveLength(1); const recursive = {} as Type; - lowerer.shapes.recursiveRef(recursive); + lowerer.shapes.recursiveRef(recursive, "Recursive"); lowerer.shapes.finalizeRecursive(recursive, [{ name: "kind", type: STRING }]); expect(lowerer.unionRetagHelper(from, to, loc)).toBe(helper); expect(plan).toHaveBeenCalled(); @@ -54,7 +55,7 @@ test("replans after a recursive union gains its final arms", () => { const lowerer = context(); const recursive = {} as Type; const from = lowerer.unions.intern([UNDEFINED_T, NULL_T]); - const to = lowerer.unions.recursiveRef(recursive); + const to = lowerer.unions.recursiveRef(recursive, "Recursive"); const trapped = lowerer.unionRetagHelper(from, to, loc); expect(trapped).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [UNDEFINED_T]); @@ -75,7 +76,7 @@ test("rechecks width conversions after recursive record definitions settle", () const lowerer = context(); const recursive = {} as Type; const source = record(lowerer.shapes.intern([{ name: "value", type: F64 }])); - const target = record(lowerer.shapes.recursiveRef(recursive)); + const target = record(lowerer.shapes.recursiveRef(recursive, "Recursive")); const from = lowerer.unions.intern([source, UNDEFINED_T]); const to = lowerer.unions.intern([target, NULL_T]); expect(lowerer.unionRetagHelper(from, to, loc)).not.toBeNull(); @@ -129,7 +130,7 @@ test("literal ownership is reused across values and rechecked when recursive rec expect(inspect).not.toHaveBeenCalled(); const recursive = {} as Type; - const pendingShape = record(lowerer.shapes.recursiveRef(recursive)); + const pendingShape = record(lowerer.shapes.recursiveRef(recursive, "Recursive")); const pending = lowerer.unions.intern([pendingShape], { field: "kind", cases: [{ tag: 0, values: ["a"] }], @@ -143,7 +144,7 @@ test("literal ownership is reused across values and rechecked when recursive rec test("literal ownership sees a recursive union's finalized discriminator", () => { const lowerer = context(); const recursive = {} as Type; - const union = lowerer.unions.recursiveRef(recursive); + const union = lowerer.unions.recursiveRef(recursive, "Recursive"); const shape = record(lowerer.shapes.intern([{ name: "kind", type: STRING }])); expect(lowerer.literalUnionArm(union, ["ready"])).toBeNull(); lowerer.unions.finalizeRecursive(recursive, [shape], { @@ -182,7 +183,10 @@ test("removing undefined reuses the discriminator transformation without hiding ); const recursive = {} as Type; - const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; + const pending: IrType = { + kind: "union", + unionId: lowerer.unions.recursiveRef(recursive, "Recursive"), + }; expect(lowerer.stripUndefinedArm(pending)).toEqual(pending); lowerer.unions.finalizeRecursive(recursive, [STRING, UNDEFINED_T]); expect(lowerer.stripUndefinedArm(pending)).toEqual(STRING); @@ -215,7 +219,10 @@ test("adding undefined reuses semantic union transformations and sees recursive expect(lowerer.unions.get(added.unionId)?.discriminant).toEqual(discriminant); const recursive = {} as Type; - const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; + const pending: IrType = { + kind: "union", + unionId: lowerer.unions.recursiveRef(recursive, "Recursive"), + }; const before = lowerer.withUndefinedArmOf(pending); lowerer.unions.finalizeRecursive(recursive, [a, b], discriminant); expect(lowerer.withUndefinedArmOf(pending)).toEqual(added); @@ -227,13 +234,18 @@ test("adding undefined reuses semantic union transformations and sees recursive test("optional union widening keeps refused and missing contracts current", () => { const lowerer = context(); const recursive = {} as Type; - const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; + const pending: IrType = { + kind: "union", + unionId: lowerer.unions.recursiveRef(recursive, "Recursive"), + }; expect(lowerer.withUndefinedArmOf(pending)).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [{ kind: "date" }, STRING]); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); - const missing: IrType = { kind: "union", unionId: `u${lowerer.unions.unions.length}` }; + // Union ids come from their arms, so a fresh registry names the union + // this one has not interned yet. + const missing: IrType = { kind: "union", unionId: new UnionRegistry().intern([F64, STRING]) }; expect(lowerer.withUndefinedArmOf(missing)).toBeNull(); expect(lowerer.unions.intern([F64, STRING])).toBe(missing.unionId); expect(lowerer.withUndefinedArmOf(missing)?.kind).toBe("union"); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ee8ba7ecba..687a2fb1af 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,4 +1,6 @@ import { relieveHeapPressure } from "../../heap-pressure.js"; +import { keyedHelperName } from "./stable-names.js"; +import { positionTag, StructuralPositions } from "./structural-positions.js"; import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js"; import { needsGeneralNodeStreams, @@ -1004,6 +1006,11 @@ function lowerShuffle(): ((wave: string[]) => void) | null { * the bodies reachable emit did NOT mark (plus deferred collection * diagnostics nothing flushed), reported separately: whole-program analysis without * letting unreached code fail builds. */ +/** The closure root of an emit-override specialization body. */ +function emitSpecRoot(spec: EmitSpecRequest): string { + return `%emit.${spec.info.def.name}.${spec.event}.${spec.ordinal}`; +} + export function lowerToIr( program: ts.Program, entry: ts.SourceFile, @@ -2801,6 +2808,8 @@ export class Lowerer { /** Generated value helpers, keyed by operation and type. Callers own * their key prefixes; this includes more than structural conversions. */ readonly valueHelpers = new Map(); + /** Each name keyedHelperName made, with a second hash of its key. */ + readonly keyedNameKeys = new Map(); /** structuralNarrowing's assignable classes, keyed by static and * narrowed class name. */ readonly structuralClassPeers = new Map(); @@ -2937,7 +2946,6 @@ export class Lowerer { ctx: FnCtx; } >(); - chainCounter = 0; /** Keyed by program-wide qualified class name (what IR object types carry). */ readonly classes = new Map(); readonly classBySymbol = new Map(); @@ -3167,7 +3175,47 @@ export class Lowerer { * dispatch revision needs no weak-key identity or process-global state. */ errorMethodDispatchRevision: string | null = null; errorPropertyDispatchRevision: string | null = null; - lambdaCounter = 0; + /** Structural source positions (structural-positions.ts) for names that + * stay the same when unrelated code moves. */ + readonly positions = new StructuralPositions(); + + /** The outermost node whose token span is exactly `loc`, or null. */ + nodeAtLoc(loc: SrcLoc): ts.Node | null { + const sf = this.program.getSourceFile(loc.file); + return sf === undefined ? null : (ts.nodeAtSpan(sf, loc.start, loc.end) ?? null); + } + + /** The lowering unit whose name scopes the closures it lifts. */ + private closureRoot = "%pre"; + private readonly closureNameCounts = new Map(); + + /** Runs `fn` with the closures it lifts named under `root`. */ + withClosureRoot(root: string, fn: () => T): T { + const previous = this.closureRoot; + this.closureRoot = root; + try { + return fn(); + } finally { + this.closureRoot = previous; + } + } + + /** The name of a closure (or wrapper) lifted for the source at `loc`: + * `.` of the current lowering unit, the location and + * `suffix`, then `suffix`. A unit that lifts several functions for the same + * location and suffix numbers them in the order it lifts them, so a + * closure's name depends only on its unit, not on which units lowered + * before it. */ + closureName(loc: SrcLoc, suffix = "", prefix = "%fn"): string { + const node = this.nodeAtLoc(loc); + const where = node === null ? `${loc.file}:${loc.start}:${loc.end}` : this.positions.of(node); + const key = `${prefix}|${this.closureRoot}|${where}|${suffix}`; + const first = keyedHelperName(this, prefix, key); + const seen = this.closureNameCounts.get(first) ?? 0; + this.closureNameCounts.set(first, seen + 1); + return (seen === 0 ? first : keyedHelperName(this, prefix, `${key}|${seen}`)) + suffix; + } + readonly preparedClassFactories = new Map(); readonly preparingClassFactories = new Set(); @@ -3808,11 +3856,11 @@ export class Lowerer { })() ? this.qualify( ts.getSourceFileOfNode(decl), - `%cx${ts.tokenStartOfNode(decl)}.${decl.name?.text ?? ""}`, + `%cx${positionTag(this, decl)}.${decl.name?.text ?? ""}`, ) : this.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(decl) + (decl.name ? decl.name.text : "%anon"), + nsPathPrefix(this, decl) + (decl.name ? decl.name.text : "%anon"), ); for (let index = this.localClassInstantiations.length - 1; index >= 0; index--) { const entry = this.localClassInstantiations[index]!; @@ -7371,7 +7419,10 @@ export class Lowerer { } for (const decl of fp.classDecls) { const info = this.classes.get(this.classNamer(decl)); - if (info) functions.push(...this.lowerClassMembers(info)); + if (info) + functions.push( + ...this.withClosureRoot(info.def.name, () => this.lowerClassMembers(info)), + ); else if (this.countsSkips()) this.countSkippedBody(decl); } } @@ -7384,7 +7435,10 @@ export class Lowerer { // are reachable by definition, already counted by reachable emit. if (!this.remainder) { for (const fp of parts) { - functions.push(this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!)); + const initName = this.initNameOf.get(fp.sf)!; + functions.push( + this.withClosureRoot(initName, () => this.lowerFileInit(fp.sf, fp.topStmts, initName)), + ); } functions.push(this.buildMain()); } @@ -7426,7 +7480,9 @@ export class Lowerer { // default): the diagnostic is recorded — the instance skips // like a signature-blocked function (lowerFunction's rule). try { - functions.push(this.lowerGenericInstance(info, inst)); + functions.push( + this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), + ); } catch (e) { if (!(e instanceof PoisonError)) throw e; } @@ -7436,7 +7492,10 @@ export class Lowerer { // instances of its own; the joint fixpoint covers both). while (es < this.emitSpecQueue.length) { try { - const fn = lowerEmitOverrideSpec(this, this.emitSpecQueue[es++]!); + const spec = this.emitSpecQueue[es++]!; + const fn = this.withClosureRoot(emitSpecRoot(spec), () => + lowerEmitOverrideSpec(this, spec), + ); if (fn) functions.push(fn); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -7990,7 +8049,9 @@ export class Lowerer { this.genericClassDemandPriority.set(info, ref); instanceFunctions.push( ...this.withGenericDemandOwner(owner, () => - this.shapes.withDeclaredOrderPriority(ref, () => this.lowerClassMembers(info)), + this.shapes.withDeclaredOrderPriority(ref, () => + this.withClosureRoot(info.def.name, () => this.lowerClassMembers(info)), + ), ), ); relieveHeapPressure(); @@ -8017,7 +8078,7 @@ export class Lowerer { instanceFunctions.push( this.withGenericDemandOwner(owner, () => this.shapes.withDeclaredOrderPriority(ref, () => - this.lowerGenericInstance(info, inst), + this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), ), ), ); @@ -8050,9 +8111,12 @@ export class Lowerer { ); while (specLowered < waveEnd) { try { - const fn = this.shapes.withDeclaredOrderPriority([4, instanceMetadataOrder++], () => - lowerEmitOverrideSpec(this, this.emitSpecQueue[specLowered++]!), - ); + const fn = this.shapes.withDeclaredOrderPriority([4, instanceMetadataOrder++], () => { + const spec = this.emitSpecQueue[specLowered++]!; + return this.withClosureRoot(emitSpecRoot(spec), () => + lowerEmitOverrideSpec(this, spec), + ); + }); if (fn) instanceFunctions.push(fn); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -8080,7 +8144,9 @@ export class Lowerer { initFunctions.push( this.withGenericDemandOwner(owner, () => this.shapes.withDeclaredOrderPriority(priority, () => - this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!), + this.withClosureRoot(this.initNameOf.get(fp.sf)!, () => + this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!), + ), ), ), ); @@ -8106,7 +8172,9 @@ export class Lowerer { const priority = metadataPriority.get(name) ?? [3, expressionMetadataOrder++]; const owner = demandOwner(priority); const fn = this.withGenericDemandOwner(owner, () => - this.shapes.withDeclaredOrderPriority(priority, unit.lower), + this.shapes.withDeclaredOrderPriority(priority, () => + this.withClosureRoot(name, unit.lower), + ), ); if (fn) loweredUnits.set(name, fn); relieveHeapPressure(); @@ -13489,7 +13557,7 @@ export class Lowerer { const key = `${fn.valueFn ?? fn.fn}:${typeKey(funcType)}:${adapterShape}`; let fnName = this.builtinCallableValueFns.get(key); if (!fnName) { - fnName = `%builtin.value.${fn.valueFn ?? fn.fn}.${this.builtinCallableValueFns.size}`; + fnName = keyedHelperName(this, `%builtin.value.${fn.valueFn ?? fn.fn}`, key); this.builtinCallableValueFns.set(key, fnName); this.builtinCallableValueNames.set(fnName, display.slice(display.lastIndexOf(".") + 1)); const params = funcType.params.map((type, index) => ({ diff --git a/packages/compiler/src/frontend/lowering/native-ffi.ts b/packages/compiler/src/frontend/lowering/native-ffi.ts index 0f932ba930..62f2be9f1f 100644 --- a/packages/compiler/src/frontend/lowering/native-ffi.ts +++ b/packages/compiler/src/frontend/lowering/native-ffi.ts @@ -562,7 +562,7 @@ export function lowerFfiMemoryModule(lowerer: Lowerer, loc: SrcLoc): IrExpr { const entries = lowerer.ffiImports .filter((entry) => entry.library !== undefined) .map((entry) => { - const name = `%ffi.module.${lowerer.lambdaCounter++}`; + const name = lowerer.closureName(loc, "", "%ffi.module"); const sourceTypes = ffiSourceParamTypes(entry.params); const locals: IrLocal[] = sourceTypes.map((_, i) => ({ id: `%arg${i}`, diff --git a/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts b/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts index 855da3bda1..388ac3904f 100644 --- a/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts +++ b/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { BOOL, STRING, @@ -27,7 +28,7 @@ export function objectEnumerationReceiver( const key = `obj.receiver:${source.unionId}:${expected.shapeId}`; let name = lowerer.arrHofHelpers.get(key); if (name === undefined) { - name = `%obj.receiver.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%obj.receiver", key); lowerer.arrHofHelpers.set(key, name); const value = varRef("value.0", source, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/pairs-snapshot.ts b/packages/compiler/src/frontend/lowering/pairs-snapshot.ts index 3b33fb0cde..acf7b3bf9b 100644 --- a/packages/compiler/src/frontend/lowering/pairs-snapshot.ts +++ b/packages/compiler/src/frontend/lowering/pairs-snapshot.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { arrayOf, BOOL, @@ -49,7 +50,7 @@ export function pairsSnapshotHelper( const key = `${options.keyPrefix}.snapshot:${shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%${options.keyPrefix}.snapshot.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, `%${options.keyPrefix}.snapshot`, key); lowerer.valueHelpers.set(key, name); const recordType: IrType = { kind: "record", shapeId }; const pairsType = arrayOf(STRING); diff --git a/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts index 09d228b05e..84aa8dac09 100644 --- a/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts +++ b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { varRef } from "../../ir/build.js"; import { type IrExpr, @@ -40,7 +41,7 @@ export function adaptPromiseExecutor( const key = `promise.executor:${typeKey(execT)}:${typeKey(inner)}`; let name = lowerer.arrHofHelpers.get(key); if (name === undefined) { - name = `%promise.executor.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%promise.executor", key); lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; const captured: IrLocal = { id: "f.0", name: "f", type: execT, mutable: false, boxed: true }; diff --git a/packages/compiler/src/frontend/lowering/require-initialization.ts b/packages/compiler/src/frontend/lowering/require-initialization.ts index d979423b0e..c8fc9c7309 100644 --- a/packages/compiler/src/frontend/lowering/require-initialization.ts +++ b/packages/compiler/src/frontend/lowering/require-initialization.ts @@ -9,6 +9,7 @@ * and every reference that can run earlier checks it first — the same * module.tdzCheck the import-cycle bindings use. A reference whose lowering * leaves no expression to check keeps the SC1013 fence. */ +import { positionTag } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { @@ -119,7 +120,7 @@ export function markRequireEarlyBindings(lowerer: Lowerer): void { const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, ""); const bySymbol = new Map(); for (const { decl } of declared) { - const flagId = `%g.${tag}%required.${ts.tokenStartOfNode(decl)}`; + const flagId = `%g.${tag}%required.${positionTag(lowerer, decl)}`; lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true }); const flag: RequireInitFlag = { flagId, names: new Map() }; for (const id of boundNames(decl.name)) { diff --git a/packages/compiler/src/frontend/lowering/stable-names.ts b/packages/compiler/src/frontend/lowering/stable-names.ts new file mode 100644 index 0000000000..8f7d0967e4 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/stable-names.ts @@ -0,0 +1,48 @@ +import { InternalCompilerError } from "../../errors.js"; + +/** The owner of the names keyedHelperName hands out. */ +export interface KeyedNameOwner { + /** Each keyed name with a second hash of the key it was made from, which + * tells a repeated key from a different key with the same name. */ + readonly keyedNameKeys: Map; +} + +/** 48 bits of two FNV-1a hashes of `key`, as 12 hex digits. */ +function keyHash(key: string): string { + let a = 0x811c9dc5; + let b = 0x01000193 ^ 0x5bd1e995; + for (let i = 0; i < key.length; i++) { + const c = key.charCodeAt(i); + a = Math.imul(a ^ c, 0x01000193); + b = Math.imul(b ^ c, 0x01000193) ^ (b >>> 13); + } + const high = (b >>> 0).toString(16).padStart(8, "0").slice(4); + return high + (a >>> 0).toString(16).padStart(8, "0"); +} + +/** A 32-bit hash of `key` independent of keyHash's bits. */ +function checkHash(key: string): number { + let h = 0x9e3779b9; + for (let i = 0; i < key.length; i++) + h = Math.imul(h ^ key.charCodeAt(i), 0x85ebca6b) ^ (h >>> 15); + return h >>> 0; +} + +/** The name of a helper interned by `key`: ``. It depends only on the key, not on how many helpers were interned + * before, so a helper has the same name whichever unit first needs it. Two + * keys that hash to the same name throw an InternalCompilerError. */ +export function keyedHelperName( + owner: KeyedNameOwner, + prefix: string, + key: string, + separator = ".", +): string { + const name = `${prefix}${separator}${keyHash(key)}`; + const check = checkHash(key); + const previous = owner.keyedNameKeys.get(name); + if (previous === undefined) owner.keyedNameKeys.set(name, check); + else if (previous !== check) + throw new InternalCompilerError(`helper name ${name} is shared by two helper keys`); + return name; +} diff --git a/packages/compiler/src/frontend/lowering/strict-equality.ts b/packages/compiler/src/frontend/lowering/strict-equality.ts index 7b9b6902b6..8a87c8111b 100644 --- a/packages/compiler/src/frontend/lowering/strict-equality.ts +++ b/packages/compiler/src/frontend/lowering/strict-equality.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { boolLit, varRef } from "../../ir/build.js"; import { BOOL, @@ -135,7 +136,7 @@ export function lowerUnionEquality( loc, ); if (!result) return null; - name = `%value.eq.${lowerer.equalityHelpers.size}`; + name = keyedHelperName(lowerer, "%value.eq", key); lowerer.equalityHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/structural-positions.ts b/packages/compiler/src/frontend/lowering/structural-positions.ts new file mode 100644 index 0000000000..cdc290b4bf --- /dev/null +++ b/packages/compiler/src/frontend/lowering/structural-positions.ts @@ -0,0 +1,103 @@ +import * as ts from "../ts7/adapter.js"; +import { keyedHelperName, type KeyedNameOwner } from "./stable-names.js"; + +/** The declaration kinds whose names anchor the positions inside them. */ +const ANCHOR_KINDS: ReadonlySet = new Set([ + ts.SyntaxKind.FunctionDeclaration, + ts.SyntaxKind.ClassDeclaration, + ts.SyntaxKind.ClassExpression, + ts.SyntaxKind.VariableDeclaration, + ts.SyntaxKind.MethodDeclaration, + ts.SyntaxKind.GetAccessor, + ts.SyntaxKind.SetAccessor, + ts.SyntaxKind.PropertyDeclaration, + ts.SyntaxKind.PropertyAssignment, + ts.SyntaxKind.ModuleDeclaration, + ts.SyntaxKind.EnumDeclaration, + ts.SyntaxKind.Constructor, +]); + +/** The name of `node` when it is a named declaration that anchors the + * positions of the nodes inside it, or null. */ +function anchorName(node: ts.Node): string | null { + return ts.declarationNameAt(node, node.index, ANCHOR_KINDS); +} + +/** The nearest enclosing anchor of `node` (not `node` itself), or its + * source file. */ +function anchorOf(node: ts.Node): ts.Node { + for (let parent = node.parent; parent !== undefined; parent = parent.parent) { + if (ts.isSourceFile(parent) || anchorName(parent) !== null) return parent; + } + return ts.getSourceFileOfNode(node); +} + +/** Source positions that stay the same when code outside the enclosing + * declaration changes. A node's structural position is its file, the chain + * of enclosing named declarations (each with its ordinal among the + * declarations of the same kind and name in its own anchor), and its + * ordinal among the nodes of its kind whose nearest anchor is the same + * declaration. Editing another declaration, or code nested in another + * named declaration of the same anchor, leaves the position unchanged. */ +export class StructuralPositions { + /** Per anchor and key: the indices of the nodes with that key whose + * nearest anchor it is, in preorder (anchoredNodes). */ + private readonly nodesByKey = new Map>(); + private readonly paths = new Map(); + + /** The ordinal of `node` among the nodes of `anchor` with its key. */ + private ordinalIn(anchor: ts.Node, node: ts.Node, key: string): number { + let byKey = this.nodesByKey.get(anchor); + if (byKey === undefined) this.nodesByKey.set(anchor, (byKey = new Map())); + let indices = byKey.get(key); + if (indices === undefined) { + indices = ts.anchoredNodes(anchor, key, ANCHOR_KINDS); + byKey.set(key, indices); + } + // Indices increase in preorder. + let low = 0; + let high = indices.length - 1; + while (low <= high) { + const middle = (low + high) >> 1; + const at = indices[middle]!; + if (at === node.index) return middle; + if (at < node.index) low = middle + 1; + else high = middle - 1; + } + return -1; + } + + /** The anchor chain of `anchor` from the file down. */ + private pathOf(anchor: ts.Node): string { + const cached = this.paths.get(anchor); + if (cached !== undefined) return cached; + let path: string; + if (ts.isSourceFile(anchor)) path = anchor.fileName; + else { + const parent = anchorOf(anchor); + const name = anchorName(anchor); + path = `${this.pathOf(parent)}/${name}#${this.ordinalIn(parent, anchor, `${anchor.kind}:${name}`)}`; + } + this.paths.set(anchor, path); + return path; + } + + /** The structural position of `node`. */ + of(node: ts.Node): string { + const anchor = anchorOf(node); + const name = anchorName(node); + const key = name === null ? `${node.kind}` : `${node.kind}:${name}`; + return `${this.pathOf(anchor)}|${key}#${this.ordinalIn(anchor, node, key)}`; + } +} + +/** The owner of structural positions and the names made from them. */ +export interface PositionTagOwner extends KeyedNameOwner { + readonly positions: StructuralPositions; +} + +/** A short tag for `node`'s structural position, for names that must stay + * unique per source node (class expressions, namespace blocks). */ +export function positionTag(owner: PositionTagOwner, node: ts.Node): string { + return keyedHelperName(owner, "", owner.positions.of(node), ""); +} diff --git a/packages/compiler/src/frontend/ts7/ast.ts b/packages/compiler/src/frontend/ts7/ast.ts index 369213d896..0bddd1a807 100644 --- a/packages/compiler/src/frontend/ts7/ast.ts +++ b/packages/compiler/src/frontend/ts7/ast.ts @@ -47,8 +47,9 @@ import { } from "./ast-guards.generated.js"; import type { Ts7DiagnosticData as Diagnostic } from "./session-schema.generated.js"; import { ModifierFlags, SyntaxKind } from "./enums.js"; -import { AstKind } from "./ast-schema.generated.js"; +import { AstKind, KIND_NODE_LIST } from "./ast-schema.generated.js"; import { PREORDER_LIST } from "./ast-wire.js"; +import { tokenStartOfNode } from "./node-functions.js"; /* ---- everything 7 kept under the same name ---- */ @@ -196,6 +197,93 @@ function walkLinkedPreorder( } } +/** The semantic children of a wire node: list items stand in for their + * list, and attached JSDoc trees are left out (the walkPreorder view). */ +function semanticChildIndices(root: Node, index: number, out: number[]): void { + const wire = root.file.wire; + for (let child = wire.firstChild(index); child !== 0; child = wire.next(child)) { + const kind = wire.kind(child); + if (kind === KIND_NODE_LIST) { + for (let item = wire.firstChild(child); item !== 0; item = wire.next(item)) out.push(item); + } else if (kind !== AstKind.JSDoc) out.push(child); + } +} + +/** The outermost node of `sourceFile` whose token span is exactly + * [start, end), or undefined. Only nodes ending at `end` are + * materialized. */ +export function nodeAtSpan(sourceFile: SourceFile, start: number, end: number): Node | undefined { + const wire = sourceFile.file.wire; + const children: number[] = []; + let index = sourceFile.index; + for (;;) { + children.length = 0; + semanticChildIndices(sourceFile, index, children); + let next = 0; + for (const child of children) { + if (wire.pos(child) > start || wire.end(child) < end) continue; + if (wire.end(child) === end) { + const node = sourceFile.file.node(child); + if (tokenStartOfNode(node) === start) return node; + } + next = child; + break; + } + if (next === 0) return undefined; + index = next; + } +} + +/** The name of the declaration at wire `index` in `root`'s file when its + * kind is in `kinds`: its identifier, private identifier or string literal + * name, "constructor" for a constructor, or null. Reads the wire without + * materializing nodes. */ +export function declarationNameAt( + root: Node, + index: number, + kinds: ReadonlySet, +): string | null { + const wire = root.file.wire; + const kind = wire.kind(index); + if (!kinds.has(kind)) return null; + if (kind === SyntaxKind.Constructor) return "constructor"; + const name = wire.namedChild(index, "name"); + if (name === 0) return null; + const nameKind = wire.kind(name); + if ( + nameKind !== SyntaxKind.Identifier && + nameKind !== SyntaxKind.PrivateIdentifier && + nameKind !== SyntaxKind.StringLiteral + ) + return null; + return wire.text(name) ?? null; +} + +/** The indices, in preorder, of the nodes below `root` that have `key` and + * are not inside a nested named declaration (declarationNameAt with + * `kinds`). A named declaration is keyed `:` and its subtree is + * not visited; other nodes are keyed by their kind. No node is + * materialized. */ +export function anchoredNodes(root: Node, key: string, kinds: ReadonlySet): number[] { + const wire = root.file.wire; + const out: number[] = []; + const children: number[] = []; + const stack: number[] = []; + semanticChildIndices(root, root.index, children); + for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]!); + while (stack.length > 0) { + const index = stack.pop()!; + const kind = wire.kind(index); + const name = declarationNameAt(root, index, kinds); + if ((name === null ? String(kind) : `${kind}:${name}`) === key) out.push(index); + if (name !== null) continue; + children.length = 0; + semanticChildIndices(root, index, children); + for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]!); + } + return out; +} + export function getLineAndCharacterOfPosition( sourceFile: SourceFile, position: number, diff --git a/packages/compiler/src/frontend/type-mapper.test.ts b/packages/compiler/src/frontend/type-mapper.test.ts index cf78554b42..a99eb44cc7 100644 --- a/packages/compiler/src/frontend/type-mapper.test.ts +++ b/packages/compiler/src/frontend/type-mapper.test.ts @@ -41,7 +41,7 @@ describe("union arm lookup", () => { test("sees finalized recursive arms after a missing lookup", () => { const unions = new UnionRegistry(); const recursive = {} as Type; - const union = unions.recursiveRef(recursive); + const union = unions.recursiveRef(recursive, "Recursive"); expect(unions.armTag(union, STRING)).toBe(-1); unions.finalizeRecursive(recursive, [ F64, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index a233f0186d..ce73e8bc25 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1,4 +1,5 @@ import { InternalCompilerError } from "../errors.js"; +import { keyedHelperName } from "./lowering/stable-names.js"; import { mockCallRecordType, mockContextType } from "./mock-shapes.js"; import * as ts from "./ts7/adapter.js"; import { bodyReadsArguments } from "./arguments-usage.js"; @@ -252,6 +253,8 @@ function comparePriority(left: DeclaredOrderPriority, right: DeclaredOrderPriori export class ShapeRegistry { /** Existing layouts change only when a recursive placeholder closes. */ revision = 0; + /** Each shape id with a second hash of its key (keyedHelperName). */ + readonly keyedNameKeys = new Map(); private readonly byKey = new Map(); private readonly byId = new Map(); /** Historical emit-order rank of the declaration-order metadata each @@ -270,7 +273,7 @@ export class ShapeRegistry { * writer after all priority candidates have closed. */ private readonly derivedDeclaredOrderFinalizers: (() => void)[] = []; private currentDeclaredOrderPriority: DeclaredOrderPriority = [0, 0]; - /** All interned shapes in first-seen (`r0`, `r1`, ...) order. */ + /** All interned shapes in first-seen order. */ readonly shapes: IrRecordShape[] = []; /** ts.Types currently being mapped — a BACK-REFERENCE to one of these is * the recursive knot (`interface TreeNode { children: TreeNode[] }`): @@ -391,10 +394,12 @@ export class ShapeRegistry { /** The shape id a back-reference to an in-progress type resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER * entry (empty fields) the outer frame finalizes. */ - recursiveRef(t: ts.Type): string { + recursiveRef(t: ts.Type, description: string): string { let id = this.recIds.get(t); if (id === undefined) { - id = `r${this.shapes.length}`; + id = keyedHelperName(this, "r", `recursive:${description}`, ""); + if (this.byId.has(id)) + throw new InternalCompilerError(`two recursive types share the description ${description}`); const shape: IrRecordShape = { id, fields: [] }; this.byId.set(id, shape); this.shapes.push(shape); @@ -463,7 +468,7 @@ export class ShapeRegistry { const key = this.keyOf(fields, tuple, indexValue); let id = this.byKey.get(key); if (id === undefined) { - id = `r${this.shapes.length}`; + id = keyedHelperName(this, "r", key, ""); const shape: IrRecordShape = { id, fields, @@ -541,6 +546,17 @@ export class ShapeRegistry { } } +/** A recursive checker type's printed form and first declaration, which + * name its placeholder whichever frame first reaches it. */ +function recursiveTypeName(t: ts.Type, ctx: Pick): string { + const symbol = t.getAliasSymbol() ?? t.getSymbol(); + const declaration = symbol?.declarations[0]; + const where = declaration + ? `${ts.getSourceFileOfNode(declaration).fileName}:${declaration.pos}` + : ""; + return `${ctx.checker.typeToString(t)}@${where}`; +} + /** The frontend's union interner — mirrors ShapeRegistry. A union's * canonical identity includes its typeKey-sorted arms and literal * discriminator contract. An arm's index in that list IS its runtime tag. @@ -548,10 +564,12 @@ export class ShapeRegistry { export class UnionRegistry { /** Existing arm/discriminator contracts change when a placeholder closes. */ revision = 0; + /** Each union id with a second hash of its key (keyedHelperName). */ + readonly keyedNameKeys = new Map(); private readonly byKey = new Map(); private readonly byId = new Map(); private readonly armTags = new Map>(); - /** All interned unions in first-seen (`u0`, `u1`, ...) order. */ + /** All interned unions in first-seen order. */ readonly unions: IrUnionDef[] = []; /** ts.Types currently being mapped — a back-reference to one is the * recursive knot passing through a union (`type Tree = Leaf | Branch` @@ -568,10 +586,12 @@ export class UnionRegistry { /** The union id a back-reference to an in-progress union resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER * entry (empty arms) the outer frame finalizes. */ - recursiveRef(t: ts.Type): string { + recursiveRef(t: ts.Type, description: string): string { let id = this.recIds.get(t); if (id === undefined) { - id = `u${this.unions.length}`; + id = keyedHelperName(this, "u", `recursive:${description}`, ""); + if (this.byId.has(id)) + throw new InternalCompilerError(`two recursive types share the description ${description}`); const def: IrUnionDef = { id, arms: [] }; this.byId.set(id, def); this.unions.push(def); @@ -621,7 +641,7 @@ export class UnionRegistry { const key = JSON.stringify([arms.map(typeKey), discriminant]); let id = this.byKey.get(key); if (id === undefined) { - id = `u${this.unions.length}`; + id = keyedHelperName(this, "u", key, ""); const def: IrUnionDef = { id, arms, ...(discriminant ? { discriminant } : {}) }; this.byKey.set(key, id); this.byId.set(id, def); @@ -4651,7 +4671,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const knownRecursive = unions.recursiveUnionFor(widened); if (knownRecursive !== undefined) return { kind: "union", unionId: knownRecursive }; if (unions.inProgress.has(widened)) { - return { kind: "union", unionId: unions.recursiveRef(widened) }; + return { + kind: "union", + unionId: unions.recursiveRef(widened, recursiveTypeName(widened, ctx)), + }; } unions.inProgress.add(widened); const sensitivityAtEntry = contextResolutions; @@ -6995,7 +7018,10 @@ function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { // frame completes (finalizeRecursive below). Backends represent records // as heap references, so the self-reference is an ordinary pointer. if (shapes.inProgress.has(widened)) { - return { kind: "record", shapeId: shapes.recursiveRef(widened) }; + return { + kind: "record", + shapeId: shapes.recursiveRef(widened, recursiveTypeName(widened, ctx)), + }; } shapes.inProgress.add(widened); const sensitivityAtEntry = contextResolutions; diff --git a/packages/compiler/test/array-pipeline-fusion.test.ts b/packages/compiler/test/array-pipeline-fusion.test.ts index f5d743265f..55cd0025ec 100644 --- a/packages/compiler/test/array-pipeline-fusion.test.ts +++ b/packages/compiler/test/array-pipeline-fusion.test.ts @@ -26,7 +26,7 @@ function helperCalls(fn: IrFunction): string[] { everyStmtList(fn.body, { expr: (e) => { if (e.kind === "call" && e.callee.startsWith("%arr.")) - calls.push(e.callee.replace(/\.\d+$/, "")); + calls.push(e.callee.replace(/\.[0-9a-f]+$/, "")); return true; }, stmt: () => true, diff --git a/packages/compiler/test/format-float-arguments.test.ts b/packages/compiler/test/format-float-arguments.test.ts index e59e023ca7..6ce9c4bc04 100644 --- a/packages/compiler/test/format-float-arguments.test.ts +++ b/packages/compiler/test/format-float-arguments.test.ts @@ -27,7 +27,7 @@ test("util.format %f lowers a boolean argument once", async () => { const helpers = mod.functions .map((f) => f.name) .filter((name) => name.startsWith("%arr.")) - .map((name) => name.replace(/\.\d+$/, "")) + .map((name) => name.replace(/\.[0-9a-f]+$/, "")) .sort(); expect(helpers).toEqual(["%arr.every", "%arr.some"]); } finally { diff --git a/packages/compiler/test/in-runtime-key-emission.test.ts b/packages/compiler/test/in-runtime-key-emission.test.ts index aebbb2ea4f..589685a7a8 100644 --- a/packages/compiler/test/in-runtime-key-emission.test.ts +++ b/packages/compiler/test/in-runtime-key-emission.test.ts @@ -55,7 +55,7 @@ const d: Record = {}; d["a"] = 1; console.log(has(d, "a"), has(d, "b")); `); - const helper = body(llvm, /sc_b?f__x25_rec_haskey_\d+/); + const helper = body(llvm, /sc_b?f__x25_rec_haskey_[0-9a-f]+/); expect(helper).toContain("@scr_map_has_str("); // Not a scan over a fresh key snapshot per probe. expect(helper).not.toContain("@scr_map_keys_js_order("); diff --git a/packages/compiler/test/type-mapper-unions.test.ts b/packages/compiler/test/type-mapper-unions.test.ts index c714df4d75..6849600028 100644 --- a/packages/compiler/test/type-mapper-unions.test.ts +++ b/packages/compiler/test/type-mapper-unions.test.ts @@ -139,7 +139,8 @@ describe("checker and synthesized optional union identity", () => { nullable.arms.filter((arm) => arm.kind !== "nullT" && arm.kind !== "undefinedT"), ), ).toBe(required.unionId); - expect(nullable.discriminant!.cases.map((entry) => entry.values)).toEqual([ + // Cases follow the arms, which are ordered by their type keys. + expect(nullable.discriminant!.cases.map((entry) => entry.values).sort()).toEqual([ ["a", "alias"], ["b"], ]); From 97e85bf238e4583ce3daff31577e0d20544c0541 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 23:35:02 -0500 Subject: [PATCH 053/395] Compile js-yaml and smol-toml statically and match Node --- package.json | 3 +- .../compiler/src/backend/llvm/lib-shared.ts | 9 +- .../lowering/builtins/date-classes.ts | 555 ++++++++++++++++++ .../src/frontend/lowering/builtins/dates.ts | 95 ++- .../src/frontend/lowering/builtins/errors.ts | 111 ++++ .../lowering/class-dynamic-dispatch.ts | 114 +++- .../lowering/class-method-mutations.ts | 3 + .../src/frontend/lowering/class-prototypes.ts | 27 + .../frontend/lowering/class-to-primitive.ts | 51 +- .../frontend/lowering/expressions/binary.ts | 53 +- .../lowering/expressions/element-access.ts | 18 +- .../lowering/expressions/object-literals.ts | 3 + .../lowering/expressions/property-access.ts | 70 ++- .../lowering/expressions/relational.ts | 64 +- .../frontend/lowering/expressions/updates.ts | 60 +- .../src/frontend/lowering/lower-calls.ts | 150 ++++- .../src/frontend/lowering/lower-classes.ts | 79 ++- .../src/frontend/lowering/lower-inspect.ts | 11 +- .../src/frontend/lowering/lower-modules.ts | 2 + .../src/frontend/lowering/lower-stmts.ts | 42 +- .../compiler/src/frontend/lowering/lowerer.ts | 28 +- .../src/frontend/lowering/private-brands.ts | 90 ++- .../src/frontend/lowering/surfaces.ts | 1 + packages/compiler/src/ir/builtin-effects.ts | 4 + .../compiler/src/ir/builtin-signatures.ts | 7 + packages/compiler/src/ir/int32-slots.ts | 1 + packages/compiler/src/ir/tiers.ts | 1 + .../7040-es5-error-constructor.mjs.json | 6 + .../7041-error-called-without-new.mjs.json | 6 + ...42-string-method-names-on-objects.mjs.json | 6 + .../tests/corpus/7043-date-set-time.mjs.json | 6 + .../7044-nested-empty-object-maps.mjs.json | 6 + ...045-instanceof-constructor-values.mjs.json | 6 + .../7046-parse-float-extra-arguments.mjs.json | 6 + .../corpus/7047-dynamic-string-index.mjs.json | 6 + .../7048-unary-number-coercions.mjs.json | 6 + .../7049-member-assignment-values.mjs.json | 6 + .../tests/corpus/7050-date-subclass.mjs.json | 6 + .../corpus/7051-date-subclass-typed.ts.json | 6 + .../7052-date-subclass-dynamic.mjs.json | 6 + ...-private-fields-untyped-receivers.mjs.json | 6 + ...054-let-bindings-with-null-writes.mjs.json | 6 + packages/runtime/src/scr_dyn.h | 38 ++ packages/runtime/src/scr_dyn_invoke.c | 10 + packages/runtime/src/scr_inspect.c | 35 +- packages/runtime/src/scr_json.c | 317 +++++++++- packages/runtime/src/scr_lib.c | 178 +++++- pnpm-lock.yaml | 19 +- tests/corpus/7040-es5-error-constructor.mjs | 79 +++ .../corpus/7041-error-called-without-new.mjs | 40 ++ .../7042-string-method-names-on-objects.mjs | 70 +++ tests/corpus/7043-date-set-time.mjs | 28 + .../corpus/7044-nested-empty-object-maps.mjs | 46 ++ .../7045-instanceof-constructor-values.mjs | 43 ++ .../7046-parse-float-extra-arguments.mjs | 9 + tests/corpus/7047-dynamic-string-index.mjs | 21 + tests/corpus/7048-unary-number-coercions.mjs | 34 ++ .../corpus/7049-member-assignment-values.mjs | 20 + tests/corpus/7050-date-subclass.mjs | 69 +++ tests/corpus/7051-date-subclass-typed.ts | 47 ++ tests/corpus/7052-date-subclass-dynamic.mjs | 43 ++ .../7053-private-fields-untyped-receivers.mjs | 72 +++ .../7054-let-bindings-with-null-writes.mjs | 37 ++ tests/fixtures/npm-static/js-yaml-cli.mjs | 63 ++ tests/fixtures/npm-static/smol-toml-cli.mjs | 68 +++ .../7040-es5-error-constructor.mjs.json | 1 + .../7041-error-called-without-new.mjs.json | 1 + ...42-string-method-names-on-objects.mjs.json | 1 + .../corpus-costs/7043-date-set-time.mjs.json | 1 + .../7044-nested-empty-object-maps.mjs.json | 1 + ...045-instanceof-constructor-values.mjs.json | 1 + .../7046-parse-float-extra-arguments.mjs.json | 1 + .../7047-dynamic-string-index.mjs.json | 1 + .../7048-unary-number-coercions.mjs.json | 1 + .../7049-member-assignment-values.mjs.json | 1 + .../corpus-costs/7050-date-subclass.mjs.json | 1 + .../7051-date-subclass-typed.ts.json | 1 + .../7052-date-subclass-dynamic.mjs.json | 1 + ...-private-fields-untyped-receivers.mjs.json | 1 + ...054-let-bindings-with-null-writes.mjs.json | 1 + tests/harness/npm-static-costs.json | 4 + tests/harness/npm-static.test.ts | 32 + 82 files changed, 3065 insertions(+), 114 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/builtins/date-classes.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7040-es5-error-constructor.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7041-error-called-without-new.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7042-string-method-names-on-objects.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7043-date-set-time.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7044-nested-empty-object-maps.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7045-instanceof-constructor-values.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7046-parse-float-extra-arguments.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7047-dynamic-string-index.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7048-unary-number-coercions.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7049-member-assignment-values.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7050-date-subclass.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7051-date-subclass-typed.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7052-date-subclass-dynamic.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7053-private-fields-untyped-receivers.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7054-let-bindings-with-null-writes.mjs.json create mode 100644 tests/corpus/7040-es5-error-constructor.mjs create mode 100644 tests/corpus/7041-error-called-without-new.mjs create mode 100644 tests/corpus/7042-string-method-names-on-objects.mjs create mode 100644 tests/corpus/7043-date-set-time.mjs create mode 100644 tests/corpus/7044-nested-empty-object-maps.mjs create mode 100644 tests/corpus/7045-instanceof-constructor-values.mjs create mode 100644 tests/corpus/7046-parse-float-extra-arguments.mjs create mode 100644 tests/corpus/7047-dynamic-string-index.mjs create mode 100644 tests/corpus/7048-unary-number-coercions.mjs create mode 100644 tests/corpus/7049-member-assignment-values.mjs create mode 100644 tests/corpus/7050-date-subclass.mjs create mode 100644 tests/corpus/7051-date-subclass-typed.ts create mode 100644 tests/corpus/7052-date-subclass-dynamic.mjs create mode 100644 tests/corpus/7053-private-fields-untyped-receivers.mjs create mode 100644 tests/corpus/7054-let-bindings-with-null-writes.mjs create mode 100644 tests/fixtures/npm-static/js-yaml-cli.mjs create mode 100644 tests/fixtures/npm-static/smol-toml-cli.mjs create mode 100644 tests/harness/corpus-costs/7040-es5-error-constructor.mjs.json create mode 100644 tests/harness/corpus-costs/7041-error-called-without-new.mjs.json create mode 100644 tests/harness/corpus-costs/7042-string-method-names-on-objects.mjs.json create mode 100644 tests/harness/corpus-costs/7043-date-set-time.mjs.json create mode 100644 tests/harness/corpus-costs/7044-nested-empty-object-maps.mjs.json create mode 100644 tests/harness/corpus-costs/7045-instanceof-constructor-values.mjs.json create mode 100644 tests/harness/corpus-costs/7046-parse-float-extra-arguments.mjs.json create mode 100644 tests/harness/corpus-costs/7047-dynamic-string-index.mjs.json create mode 100644 tests/harness/corpus-costs/7048-unary-number-coercions.mjs.json create mode 100644 tests/harness/corpus-costs/7049-member-assignment-values.mjs.json create mode 100644 tests/harness/corpus-costs/7050-date-subclass.mjs.json create mode 100644 tests/harness/corpus-costs/7051-date-subclass-typed.ts.json create mode 100644 tests/harness/corpus-costs/7052-date-subclass-dynamic.mjs.json create mode 100644 tests/harness/corpus-costs/7053-private-fields-untyped-receivers.mjs.json create mode 100644 tests/harness/corpus-costs/7054-let-bindings-with-null-writes.mjs.json diff --git a/package.json b/package.json index 5cc30d9f80..69177e0e6d 100644 --- a/package.json +++ b/package.json @@ -40,10 +40,11 @@ "@scriptc/types": "workspace:*", "@types/node": "^24.0.0", "effect": "3.22.2", - "js-yaml": "4.3.0", + "js-yaml": "4.3.2", "oxfmt": "0.71.0", "oxlint": "1.86.0", "rollup": "4.62.2", + "smol-toml": "1.7.1", "three": "0.186.1", "tsx": "^4.19.0", "typescript": "5.9.3", diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8bb8bc3aab..5669704d0b 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -997,12 +997,15 @@ export const LIB_FN_SYMS: Record = { "dyn.nativeSetIs": "scr_dyn_native_set_is", "dyn.nativeMapIs": "scr_dyn_native_map_is", "dyn.nativeUrlIs": "scr_dyn_native_url_is", - "dyn.nativeDateIs": "scr_dyn_native_date_is", + "dyn.nativeDateIs": "scr_dyn_date_instance_is", + "dyn.nativeDateHandleIs": "scr_dyn_native_date_is", "dyn.globalConstructor": "scr_dyn_global_constructor", "dyn.builtinFunction": "scr_dyn_builtin_function", "date.nativeNew": "scr_dyn_native_date_new", "date.nativeNewFields": "scr_dyn_native_date_new_fields", "date.nativeNewValue": "scr_dyn_native_date_new_value", + "date.argumentsTime": "scr_dyn_date_arguments_time", + "date.classOperation": "scr_date_class_operation_install", "date.checkedValue": "scr_dyn_native_date_value", "dyn.nativeRegexIs": "scr_dyn_native_regex_is", "dyn.toString": "scr_dyn_to_string_argument", @@ -1478,6 +1481,10 @@ export const LIB_FN_SYMS: Record = { "dyn.arrayPrototype": "scr_dyn_array_prototype", "dyn.arrayConstructor": "scr_dyn_array_constructor", "dyn.objectPrototype": "scr_dyn_object_prototype", + "dyn.errorPrototype": "scr_dyn_error_prototype", + "dyn.inheritsErrorPrototype": "scr_dyn_inherits_error_prototype", + "dyn.errorCaptureStackTrace": "scr_dyn_error_capture_stack_trace", + "dyn.errorCaptureStackTraceFunction": "scr_dyn_error_capture_stack_trace_function", "dyn.functionApply": "scr_dyn_function_apply", "dyn.builtinMethod": "scr_dyn_builtin_method", "dyn.classPrototype": "scr_dyn_class_prototype", diff --git a/packages/compiler/src/frontend/lowering/builtins/date-classes.ts b/packages/compiler/src/frontend/lowering/builtins/date-classes.ts new file mode 100644 index 0000000000..ac6fe33a9f --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/date-classes.ts @@ -0,0 +1,555 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { locOf } from "../../program.js"; +import { type ClassInfo } from "../lower-classes.js"; +import { DATE_METHOD_HINT, dateJsonType, dateValueMethod } from "./dates.js"; +import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; +import { + BOOL, + DATE_T, + DYN, + F64, + NULL_T, + STRING, + VOID, + typeKey, + type IrExpr, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../../ir/ir.js"; + +/** The hidden field holding the time value of a class declared `extends + * Date`, the first field of its layout. */ +export const DATE_TIME_FIELD = "%time"; + +/** The class declared `extends Date` at the root of `info`'s base chain, or + * null. */ +export function dateRootOf(info: ClassInfo | null | undefined): ClassInfo | null { + for (let c = info ?? null; c; c = c.base) if (c.extendsDate) return c; + return null; +} + +/** Whether the program declares a class that extends Date. */ +export function programDeclaresDateClass(lowerer: Lowerer): boolean { + for (const info of lowerer.classes.values()) if (info.extendsDate) return true; + return false; +} + +/** The Date.prototype methods a class declared `extends Date` inherits. + * Those without a lowering refuse at the call. */ +const DATE_PROTOTYPE_METHODS: ReadonlySet = new Set([ + "getTime", + "valueOf", + "toISOString", + "toUTCString", + "toJSON", + "toString", + "toDateString", + "toTimeString", + "toGMTString", + "toLocaleString", + "toLocaleDateString", + "toLocaleTimeString", + "getFullYear", + "getUTCFullYear", + "getYear", + "getMonth", + "getUTCMonth", + "getDate", + "getUTCDate", + "getDay", + "getUTCDay", + "getHours", + "getUTCHours", + "getMinutes", + "getUTCMinutes", + "getSeconds", + "getUTCSeconds", + "getMilliseconds", + "getUTCMilliseconds", + "getTimezoneOffset", + "setTime", + "setFullYear", + "setUTCFullYear", + "setYear", + "setMonth", + "setUTCMonth", + "setDate", + "setUTCDate", + "setHours", + "setUTCHours", + "setMinutes", + "setUTCMinutes", + "setSeconds", + "setUTCSeconds", + "setMilliseconds", + "setUTCMilliseconds", +]); + +export const DATE_PROTOTYPE_METHOD_NAMES: readonly string[] = [...DATE_PROTOTYPE_METHODS]; + +export function isDatePrototypeMethod(name: string): boolean { + return DATE_PROTOTYPE_METHODS.has(name); +} + +/** Whether `name` is a Date method an instance of `info` inherits from + * Date.prototype: its class chain is rooted at a class declared `extends + * Date` and declares no member of that name. The runtime's view of the + * instance answers it (scr_dyn_date_class_method). */ +export function dateInheritedMember(lowerer: Lowerer, info: ClassInfo, name: string): boolean { + return ( + dateRootOf(info) !== null && + DATE_PROTOTYPE_METHODS.has(name) && + !lowerer.findMethodOn(info, name) && + !info.fields.has(name) + ); +} + +/** super(...args) in a class declared `extends Date`: the time value the + * Date constructor computes from the argument vector `args` (one dyn + * array), stored in the hidden field. */ +export function dateSuperCall(thisRef: IrExpr, info: ClassInfo, args: IrExpr, loc: SrcLoc): IrStmt { + return { + kind: "fieldSet", + obj: thisRef, + className: info.def.name, + field: DATE_TIME_FIELD, + value: { kind: "libCall", fn: "date.argumentsTime", args: [args], type: DATE_T, loc }, + loc, + }; +} + +/** The time value of `receiver`, an instance of a class rooted at `root`. */ +function timeOf(lowerer: Lowerer, receiver: IrExpr, root: ClassInfo, loc: SrcLoc): IrExpr { + return { + kind: "fieldGet", + obj: lowerer.upcastTo(receiver, root.def.name), + className: root.def.name, + field: DATE_TIME_FIELD, + type: DATE_T, + loc, + }; +} + +/** `receiver.name()` with no arguments where the static class `info` does + * not declare `name`: the subclasses of `info` that declare it run theirs, + * any other instance the Date method. `receiver` is a variable read. */ +function instanceMethodCall( + lowerer: Lowerer, + blame: ts.Node, + info: ClassInfo, + receiver: IrExpr, + name: string, + inherited: IrExpr, + loc: SrcLoc, +): IrExpr { + const found = lowerer.findMethodOn(info, name); + if (found) { + if (found.sig.params.length > 0) + lowerer.unsupported( + "SC1090", + blame, + `a '${name}' declaration with parameters in a class that extends Date, called by an inherited Date method`, + ); + const args: IrExpr[] = []; + if (lowerer.overrideBelow(info, name)) { + lowerer.noteVirtualEdge(info, name); + return { + kind: "virtualCall", + className: info.def.name, + method: name, + args: [lowerer.upcastTo(receiver, info.def.name), ...args], + type: found.sig.ret, + loc, + }; + } + lowerer.noteEdge(`%${found.declarer.def.name}.${name}`); + return { + kind: "call", + callee: `%${found.declarer.def.name}.${name}`, + args: [lowerer.upcastTo(receiver, found.declarer.def.name), ...args], + type: found.sig.ret, + loc, + }; + } + let result = inherited; + // Deeper declarations are tested first. + const declaring: ClassInfo[] = []; + const visit = (c: ClassInfo): void => { + for (const sub of c.subclasses) { + if (sub.methods.has(name)) declaring.push(sub); + else visit(sub); + } + }; + visit(info); + for (const sub of declaring) { + const call = instanceMethodCall( + lowerer, + blame, + sub, + { kind: "downcast", value: receiver, type: { kind: "object", className: sub.def.name }, loc }, + name, + inherited, + loc, + ); + const type = result.type; + result = { + kind: "ternary", + cond: { kind: "instanceOf", value: receiver, className: sub.def.name, type: BOOL, loc }, + then: lowerer.coerceToExpected(call, type), + else_: result, + type, + loc, + }; + } + return result; +} + +/** ToNumber of a call argument. */ +function numberArgument(lowerer: Lowerer, node: ts.Expression | undefined, loc: SrcLoc): IrExpr { + if (!node) return { kind: "numLit", value: Number.NaN, type: F64, loc }; + const value = lowerer.lowerExpr(node); + if (value.type.kind === "f64") return value; + if (value.type.kind === "dyn") + return { kind: "libCall", fn: "dyn.toNumberCoerce", args: [value], type: F64, loc }; + if (lowerer.dynConvertible(value.type)) + return { + kind: "libCall", + fn: "dyn.toNumberCoerce", + args: [{ kind: "dynFrom", value, type: DYN, loc }], + type: F64, + loc, + }; + return lowerer.coerceInto(node, value, F64); +} + +/** A Date.prototype method a class rooted at `root` does not declare, + * called on `receiver` (an instance of `info`, evaluated here once). The + * method reads or sets the hidden time value; toJSON calls the instance's + * toISOString, as Date.prototype.toJSON does. */ +export function lowerInheritedDateMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + info: ClassInfo, + receiver: IrExpr, +): IrExpr { + const root = dateRootOf(info)!; + const loc = locOf(call); + const local = lowerer.declareHiddenLocal("%dateReceiver", receiver.type); + const value = varRef(local.id, receiver.type, loc); + const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }]; + const args = call.arguments.nodes; + if (args.some(ts.isSpreadElement)) + lowerer.unsupported("SC1090", call, `spread arguments to 'Date.prototype.${name}'`); + const done = (result: IrExpr): IrExpr => ({ + kind: "seqExpr", + stmts, + result, + type: result.type, + loc, + }); + if (name === "setTime") { + const time = lowerer.declareHiddenLocal("%dateTime", DATE_T); + stmts.push( + { + kind: "varDecl", + localId: time.id, + init: { + kind: "libCall", + fn: "date.newMs", + args: [numberArgument(lowerer, args[0], loc)], + type: DATE_T, + loc, + }, + loc, + }, + ...args.slice(1).map((arg): IrStmt => ({ + kind: "exprStmt", + expr: lowerer.lowerExpr(arg), + loc: locOf(arg), + })), + { + kind: "fieldSet", + obj: lowerer.upcastTo(value, root.def.name), + className: root.def.name, + field: DATE_TIME_FIELD, + value: varRef(time.id, DATE_T, loc), + loc, + }, + ); + return done({ + kind: "libCall", + fn: "date.getTime", + args: [varRef(time.id, DATE_T, loc)], + type: F64, + loc, + }); + } + for (const arg of args) { + if (name !== "toJSON") + lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT); + stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) }); + } + if (name === "toJSON") return done(dateClassToJson(lowerer, call, info, value, loc)); + const result = dateValueMethod(name, timeOf(lowerer, value, root, loc), loc); + if (!result) lowerer.noLowering(`Date.prototype.${name}`, call, DATE_METHOD_HINT); + return done(result); +} + +/** Date.prototype.toJSON on `value` (a variable read of an instance of + * `info`): null for an invalid time, else the instance's toISOString(). */ +function dateClassToJson( + lowerer: Lowerer, + blame: ts.Node, + info: ClassInfo, + value: IrExpr, + loc: SrcLoc, +): IrExpr { + const root = dateRootOf(info)!; + const time: IrExpr = { + kind: "libCall", + fn: "date.getTime", + args: [timeOf(lowerer, value, root, loc)], + type: F64, + loc, + }; + const type = dateJsonType(lowerer); + const iso = instanceMethodCall( + lowerer, + blame, + info, + value, + "toISOString", + { + kind: "libCall", + fn: "date.toISOStringValue", + args: [timeOf(lowerer, value, root, loc)], + type: STRING, + loc, + }, + loc, + ); + return { + kind: "ternary", + // An invalid date's time value is NaN, the one value unequal to itself. + cond: { kind: "bin", op: "!==", left: time, right: time, type: BOOL, loc }, + then: lowerer.coerceToExpected({ kind: "unitLit", unit: "null", type: NULL_T, loc }, type), + else_: lowerer.coerceToExpected(iso, type), + type, + loc, + }; +} + +/** `super.name(...)` in a class whose base chain declares no `name`, when + * the chain is rooted at a class declared `extends Date`. */ +export function lowerDateSuperMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + cls: ClassInfo, + thisRef: IrExpr, +): IrExpr | null { + if (!dateRootOf(cls) || !isDatePrototypeMethod(name)) return null; + // Date.prototype.toJSON reads this.toISOString, which this class or a + // subclass may declare. + return lowerInheritedDateMethodCall(lowerer, call, name, cls, thisRef); +} + +const DATE_CLASS_TIME = "%date.classTime"; +const DATE_CLASS_METHOD = "%date.classMethod"; + +/** The classes of a program rooted at a class declared `extends Date`. */ +function dateClasses(lowerer: Lowerer): ClassInfo[] { + return [...lowerer.classes.values()].filter( + (info) => !info.def.runtime && !info.generic && dateRootOf(info) !== null, + ); +} + +const PARAMS = ["op", "value", "argument"]; + +/** A lifted `(op, value, argument)` function over checked-dynamic values. */ +function liftOperation(lowerer: Lowerer, name: string, body: IrStmt[], loc: SrcLoc): void { + lowerer.liftedFns.push({ + name, + params: PARAMS.map((p) => ({ localId: p, name: p, type: DYN })), + returnType: DYN, + locals: PARAMS.map((p) => ({ id: p, name: p, type: DYN, mutable: false })), + body, + loc, + }); +} + +function opIs(code: number, loc: SrcLoc): IrExpr { + return { + kind: "bin", + op: "===", + left: { kind: "dynCheck", value: varRef("op", DYN, loc), type: F64, loc }, + right: { kind: "numLit", value: code, type: F64, loc }, + type: BOOL, + loc, + }; +} + +/** Lifts `%date.classMethod(op, value, argument)` when the program declares + * a class that extends Date: ops 2 to 5 call `value`'s toISOString, + * toJSON(argument), toString and valueOf as checked-dynamic method calls, + * which the class dispatch resolves to each class's declarations and the + * runtime to the inherited Date methods. Called before the class dispatch + * runs over the lowered functions. */ +export function noteDateClassMethods(lowerer: Lowerer): void { + if (lowerer.liftedFnNamed(DATE_CLASS_METHOD)) return; + const classes = dateClasses(lowerer); + if (classes.length === 0) return; + const loc = classes[0]!.def.loc; + const value = varRef("value", DYN, loc); + const body: IrStmt[] = ["toISOString", "toJSON", "toString", "valueOf"].map( + (method, index): IrStmt => ({ + kind: "if", + cond: opIs(index + 2, loc), + then: [ + { + kind: "return", + value: { + kind: "dynInvoke", + recv: value, + method, + calleeName: method, + args: method === "toJSON" ? [varRef("argument", DYN, loc)] : [], + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }), + ); + body.push({ kind: "return", value: dynUndefinedExpr(loc), loc }); + liftOperation(lowerer, DATE_CLASS_METHOD, body, loc); +} + +/** %main's installation of the functions the runtime calls for a + * checked-dynamic value that holds an instance of a class declared `extends + * Date` (scr_date_class_operation_install). `%date.classTime(op, value, + * argument)` answers the time value for op 0 (undefined for any other + * value), sets it from the number `argument` for op 1 and answers the + * class's name for op 6; `%date.classMethod` (noteDateClassMethods) calls + * its methods. */ +export function dateClassOperationPrelude(lowerer: Lowerer, loc: SrcLoc): IrStmt[] { + const classes = dateClasses(lowerer); + if (classes.length === 0) return []; + noteDateClassMethods(lowerer); + const value = varRef("value", DYN, loc); + const argument = varRef("argument", DYN, loc); + const body: IrStmt[] = []; + for (const info of classes) { + const root = dateRootOf(info)!; + const type: IrType = { kind: "object", className: info.def.name }; + const instance: IrExpr = { kind: "dynCheck", value, type, loc }; + body.push({ + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.typedRefIs", + args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], + type: BOOL, + loc, + }, + then: [ + { + kind: "if", + cond: opIs(6, loc), + then: [ + { + kind: "return", + value: { + kind: "dynFrom", + value: { + kind: "strLit", + value: info.def.jsName ?? info.def.name, + type: STRING, + loc, + }, + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + { + kind: "if", + cond: opIs(1, loc), + then: [ + { + kind: "fieldSet", + obj: lowerer.upcastTo(instance, root.def.name), + className: root.def.name, + field: DATE_TIME_FIELD, + value: { + kind: "libCall", + fn: "date.newMs", + args: [{ kind: "dynCheck", value: argument, type: F64, loc }], + type: DATE_T, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + { + kind: "return", + value: { + kind: "dynFrom", + value: { + kind: "libCall", + fn: "date.getTime", + args: [timeOf(lowerer, instance, root, loc)], + type: F64, + loc, + }, + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }); + } + body.push({ kind: "return", value: dynUndefinedExpr(loc), loc }); + liftOperation(lowerer, DATE_CLASS_TIME, body, loc); + const closure = (fnName: string): IrExpr => ({ + kind: "dynFrom", + value: { + kind: "closure", + fnName, + captures: [], + type: { kind: "func", params: [DYN, DYN, DYN], ret: DYN }, + loc, + }, + type: DYN, + loc, + }); + return [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "date.classOperation", + args: [closure(DATE_CLASS_TIME), closure(DATE_CLASS_METHOD)], + type: VOID, + loc, + }, + loc, + }, + ]; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index 31be995e7a..a598f4ee6b 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -1,6 +1,8 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; import { locOf } from "../../program.js"; +import { tryLowerExpression } from "../expressions/try-lower-expression.js"; +import { programDeclaresDateClass } from "./date-classes.js"; import { BOOL, F64, @@ -9,6 +11,7 @@ import { type IrStmt, type IrType, NULL_T, + type SrcLoc, STRING, typeKey, } from "../../../ir/ir.js"; @@ -39,7 +42,7 @@ const DATE_TEXT_FNS: ReadonlyMap = new Map([ ["toTimeString", "date.toTimeString"], ]); -const DATE_METHOD_HINT = +export const DATE_METHOD_HINT = "getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering"; /** The Date slice: statics plus read-only Date values backed by one @@ -52,7 +55,52 @@ export function lowerDateCall( ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; const loc = locOf(call); - if (lowerer.stdlibGlobalMember(access, "Date") === "now") { + const member = lowerer.stdlibGlobalMember(access, "Date"); + if (member === "now" || member === "parse" || member === "UTC") + return lowerDateStaticCall(lowerer, call, member); + if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date") return null; + if (!lowerer.isStdlibMember(access)) return null; + const name = access.name.text; + // A Date a JavaScript file constructs is a mutable object; setTime on + // it takes the dynamic method call. So does every Date method on a + // checked-dynamic receiver when the program declares a class that extends + // Date: the value can be an instance, whose class may declare the method. + if ( + (name === "setTime" || programDeclaresDateClass(lowerer)) && + tryLowerExpression(lowerer, access.expression)?.type.kind === "dyn" + ) + return null; + if (name === "toJSON") return lowerDateToJson(lowerer, call, access); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT); + } + const raw = lowerer.lowerExpr(access.expression); + const receiver: IrExpr = + raw.type.kind === "dyn" + ? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: { kind: "date" }, loc } + : raw; + if (receiver.type.kind !== "date") + lowerer.badType(access.expression, lowerer.typeOf(access.expression)); + const result = dateValueMethod(name, receiver, loc); + if (result) return result; + lowerer.noLowering( + `Date.prototype.${name}`, + call, + DATE_METHOD_HINT, + lowerer.checker.getSymbolAtLocation(access.name), + ); +} + +/** Date.now(), Date.parse(text) and Date.UTC(...), also called through a + * class declared `extends Date`, which inherits them. Null for other + * names. */ +export function lowerDateStaticCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, +): IrExpr | null { + const loc = locOf(call); + if (name === "now") { if (call.arguments.nodes.length !== 0) { lowerer.noLowering(`Date.now with ${call.arguments.nodes.length} arguments`, call); } @@ -60,7 +108,7 @@ export function lowerDateCall( } // Date.parse uses the same date-string parser as new Date(dateString) // (V8's grammar in the runtime), so their NaN and TimeClip behavior agree. - if (lowerer.stdlibGlobalMember(access, "Date") === "parse") { + if (name === "parse") { if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) { lowerer.noLowering(`Date.parse with ${call.arguments.nodes.length} arguments`, call); } @@ -72,7 +120,7 @@ export function lowerDateCall( // TimeClip. Omitted trailing arguments complete with the spec's // defaults (month 0, date 1, time parts 0); tsc pins every present // argument to number, so the seven-f64 ABI is exact. - if (lowerer.stdlibGlobalMember(access, "Date") === "UTC") { + if (name === "UTC") { if ( call.arguments.nodes.length < 1 || call.arguments.nodes.length > 7 || @@ -92,49 +140,38 @@ export function lowerDateCall( } return { kind: "libCall", fn: "date.utc", args, type: F64, loc }; } - if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date") return null; - if (!lowerer.isStdlibMember(access)) return null; - const name = access.name.text; - if (name === "toJSON") return lowerDateToJson(lowerer, call, access); - if (call.arguments.nodes.length !== 0) { - lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT); - } - const raw = lowerer.lowerExpr(access.expression); - const receiver: IrExpr = - raw.type.kind === "dyn" - ? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: { kind: "date" }, loc } - : raw; - if (receiver.type.kind !== "date") - lowerer.badType(access.expression, lowerer.typeOf(access.expression)); + return null; +} + +/** The Date methods without arguments that have a lowering, applied to the + * time value `value` (a `date`): getTime, valueOf, toISOString, + * toUTCString, the calendar getters and the text forms. Null for other + * names. */ +export function dateValueMethod(name: string, value: IrExpr, loc: SrcLoc): IrExpr | null { if (name === "getTime" || name === "valueOf") { return { kind: "libCall", fn: name === "getTime" ? "date.getTime" : "date.valueOf", - args: [receiver], + args: [value], type: F64, loc, }; } if (name === "toISOString") { - return { kind: "libCall", fn: "date.toISOStringValue", args: [receiver], type: STRING, loc }; + return { kind: "libCall", fn: "date.toISOStringValue", args: [value], type: STRING, loc }; } if (name === "toUTCString") { - return { kind: "libCall", fn: "date.toUTCString", args: [receiver], type: STRING, loc }; + return { kind: "libCall", fn: "date.toUTCString", args: [value], type: STRING, loc }; } const fn = own(DATE_GETTER_FNS, name); if (fn !== undefined) { - return { kind: "libCall", fn, args: [receiver], type: F64, loc }; + return { kind: "libCall", fn, args: [value], type: F64, loc }; } const textFn = DATE_TEXT_FNS.get(name); if (textFn !== undefined) { - return { kind: "libCall", fn: textFn, args: [receiver], type: STRING, loc }; + return { kind: "libCall", fn: textFn, args: [value], type: STRING, loc }; } - lowerer.noLowering( - `Date.prototype.${name}`, - call, - DATE_METHOD_HINT, - lowerer.checker.getSymbolAtLocation(access.name), - ); + return null; } /** `d.toJSON(key?)`: Node answers null for an invalid date and the diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts index d954428294..1371769d6b 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -1,6 +1,8 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; +import { tryLowerExpression } from "../expressions/try-lower-expression.js"; +import { dynUndefinedExpr } from "../../../ir/build.js"; import { BOOL, CAUGHT, @@ -8,6 +10,7 @@ import { DYN_CLASS_PROPERTIES, F64, type IrExpr, + type IrStmt, type IrType, STRING, } from "../../../ir/ir.js"; @@ -267,6 +270,114 @@ function isErrorClassType(lowerer: Lowerer, type: IrType): boolean { ); } +/** Error classes that construct when called without `new`. */ +const CALLABLE_ERROR_CLASSES: ReadonlySet = new Set([ + "%Error", + "%TypeError", + "%RangeError", + "%SyntaxError", + "%ReferenceError", + "%EvalError", + "%URIError", +]); + +/** `Error(message?, options?)` and `Error.call(thisArg, message?, options?)` + * (an ES5 constructor function's `Error.call(this)`), and the same for the + * other standard error classes: called as a function, the constructor + * returns a new error and ignores the receiver. `thisArg` is evaluated + * first. Null for other callees. */ +export function lowerErrorFunctionCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + if (call.questionDotToken) return null; + let callee = call.expression; + let args = call.arguments.nodes; + let thisArg: ts.Expression | undefined; + const viaCall = + ts.isPropertyAccessExpression(callee) && + !callee.questionDotToken && + callee.name.text === "call" && + ts.isIdentifier(callee.expression); + if (viaCall) { + callee = (callee as ts.PropertyAccessExpression).expression; + thisArg = args[0]; + args = args.slice(1); + } + if (!ts.isIdentifier(callee) || args.some(ts.isSpreadElement)) return null; + if (thisArg && ts.isSpreadElement(thisArg)) return null; + const info = lowerer.builtinErrorInfoOf(lowerer.checker.getSymbolAtLocation(callee)); + if (!info || !CALLABLE_ERROR_CLASSES.has(info.def.name)) return null; + const loc = locOf(call); + const stmts: IrStmt[] = []; + if (thisArg) { + const receiver = lowerer.lowerExpr(thisArg); + if (receiver.kind !== "varRef" && receiver.kind !== "unitLit") + stmts.push({ kind: "exprStmt", expr: receiver, loc: receiver.loc }); + } + const error: IrExpr = { + kind: "libCall", + fn: "error.newOptions", + args: lowerer.errorConstructorArgs(args, loc, call), + type: { kind: "object", className: info.def.name }, + loc, + }; + return stmts.length > 0 + ? { kind: "seqExpr", stmts, result: error, type: error.type, loc } + : error; +} + +/** `Error.captureStackTrace(target[, constructorOpt])` (also called as + * `Error.captureStackTrace?.(...)`, which always calls it). On a + * checked-dynamic target (`this` in a JavaScript constructor function) the + * runtime defines target's `stack`. An instance of a class that extends + * Error derives its stack from its name and message when the stack is + * read, so the call only evaluates its arguments. Both arguments are + * evaluated; the second only selects frames in Node. Null for other + * targets and callees. */ +export function lowerErrorCaptureStackTraceCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.questionDotToken) return null; + if (lowerer.stdlibGlobalMember(access, "Error") !== "captureStackTrace") return null; + const args = call.arguments.nodes; + if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null; + const probed = tryLowerExpression(lowerer, args[0]!); + const loc = locOf(call); + if ( + probed?.type.kind === "object" && + (probed.type.className === "%Error" || lowerer.isSubclassOf(probed.type.className, "%Error")) + ) { + const stmts: IrStmt[] = []; + for (const arg of args) { + const value = lowerer.lowerExpr(arg); + if (value.kind !== "varRef") stmts.push({ kind: "exprStmt", expr: value, loc: value.loc }); + } + const result = dynUndefinedExpr(loc); + return stmts.length > 0 ? { kind: "seqExpr", stmts, result, type: DYN, loc } : result; + } + if (probed?.type.kind !== "dyn") return null; + const target = lowerer.lowerExpr(args[0]!); + const result: IrExpr = { + kind: "libCall", + fn: "dyn.errorCaptureStackTrace", + args: [target], + type: DYN, + loc, + }; + if (args.length === 1) return result; + const local = lowerer.declareHiddenLocal("%captureTarget", DYN); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: local.id, init: target, loc }, + { kind: "exprStmt", expr: lowerer.lowerExprExpecting(args[1]!, DYN), loc }, + ], + result: { ...result, args: [{ kind: "varRef", localId: local.id, type: DYN, loc }] }, + type: DYN, + loc, + }; +} + /** `Error.isError(value)`. A catch binding tests its payload against the * Error class, an `unknown` value tests the checked-dynamic tree's error * encoding, and a union tests its runtime tag against the arms whose class diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index c90e1135b4..fefe85d0c4 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -2,8 +2,17 @@ import { primitiveConstructorOr } from "./lower-exprs.js"; import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js"; import { constructedValue } from "./constructor-returns.js"; import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js"; +import { + DATE_PROTOTYPE_METHOD_NAMES, + DATE_TIME_FIELD, + dateInheritedMember, + dateRootOf, + programDeclaresDateClass, +} from "./builtins/date-classes.js"; +import { dateValueMethod } from "./builtins/dates.js"; import { BOOL, + DATE_T, DYN, F64, DYN_CLASS_PROPERTIES as PROPERTY_BAG, @@ -296,6 +305,32 @@ export function classInstanceOf( ); const body: IrStmt[] = prototype ? [ + // In a program that declares a class extending Date, a Date can + // meet a class test (`date instanceof Moment`): its prototype + // chain holds no class prototype. + ...(programDeclaresDateClass(lowerer) + ? [ + { + kind: "if" as const, + cond: { + kind: "libCall" as const, + fn: "dyn.nativeDateHandleIs" as const, + args: [varRef("value", DYN, loc)], + type: BOOL, + loc, + }, + then: [ + { + kind: "return" as const, + value: { kind: "boolLit" as const, value: false, type: BOOL, loc }, + loc, + }, + ], + else_: null, + loc, + }, + ] + : []), { kind: "varDecl", localId: "candidate", init: varRef("value", DYN, loc), loc }, { kind: "while", @@ -2871,6 +2906,7 @@ export class ClassDynamicDispatch { !isClassOwnEnumerableFieldName(dispatch.name) || (!(info.builtinStream !== undefined && streamProperty(dispatch.name)) && !eventTargetInheritedMember(info, dispatch.name) && + !dateInheritedMember(lowerer, info, dispatch.name) && !findMethodOn(lowerer, info, dispatch.name) && !findGenericMethodOn(lowerer, info, dispatch.name) && dispatch.name !== "constructor" && @@ -2987,7 +3023,8 @@ export class ClassDynamicDispatch { ]; if ( ((info.builtinStream !== undefined && streamProperty(name)) || - eventTargetInheritedMember(info, name)) && + eventTargetInheritedMember(info, name) || + dateInheritedMember(lowerer, info, name)) && dispatch.probe ) return [ @@ -3200,6 +3237,9 @@ export class ClassDynamicDispatch { for (const name of [...STREAM_BOOL_PROPERTIES, ...STREAM_NUM_PROPERTIES]) names.add(name); const inherited = eventTargetApiMembers(info); if (inherited) for (const name of inherited) names.add(name); + if (dateRootOf(info)) + for (const name of DATE_PROTOTYPE_METHOD_NAMES) + if (dateInheritedMember(lowerer, info, name)) names.add(name); for (let owner: ClassInfo | null = info; owner; owner = owner.base) { const methodNames = [...owner.methods.keys()]; @@ -3863,6 +3903,70 @@ export class ClassDynamicDispatch { ]; return { kind: "if", cond: condition, then: body, else_: null, loc }; } + // Date.prototype[Symbol.toPrimitive] takes "default" as "string", and a + // class declared `extends Date` inherits Date's toString and valueOf, + // which answer primitives from the time value. + const dateRoot = dateRootOf(info); + if (dateRoot) { + const dateOrder: ("valueOf" | "toString")[] = + hint === "number" ? ["valueOf", "toString"] : ["toString", "valueOf"]; + const { receiver } = this.propertyReceiver(plan, loc); + const body: IrStmt[] = []; + for (const method of dateOrder) { + if (!conversionMethod(lowerer, info, method)) { + const time: IrExpr = { + kind: "fieldGet", + obj: upcastTo(lowerer, receiver, dateRoot.def.name), + className: dateRoot.def.name, + field: DATE_TIME_FIELD, + type: DATE_T, + loc, + }; + body.push({ + kind: "return", + value: lowerer.coerceToExpected(dateValueMethod(method, time, loc)!, DYN), + loc, + }); + break; + } + const result = `%toPrimitive.${method}.${helper.locals.length}`; + helper.locals.push({ id: result, name: method, type: DYN, mutable: false }); + body.push( + { + kind: "varDecl", + localId: result, + init: { + kind: "call", + callee: this.toPrimitiveCall(lowerer, plan, method, 0, loc), + args: [varRef("p.0", DYN, loc)], + type: DYN, + loc, + }, + loc, + }, + { + kind: "if", + cond: isPrimitive(varRef(result, DYN, loc), loc), + then: [{ kind: "return", value: varRef(result, DYN, loc), loc }], + else_: null, + loc, + }, + ); + } + if (body.at(-1)?.kind !== "return") + body.push({ + kind: "return", + value: nodeThrowExpr(1, "", "Cannot convert object to primitive value", DYN, loc), + loc, + }); + return { + kind: "if", + cond: this.propertyReceiver(plan, loc).condition, + then: body, + else_: null, + loc, + }; + } const order: ("valueOf" | "toString")[] = hint === "string" ? ["toString", "valueOf"] : ["valueOf", "toString"]; if (!order.some((method) => conversionMethod(lowerer, info, method))) return null; @@ -4731,9 +4835,11 @@ export class ClassDynamicDispatch { ): IrStmt[] { const { name, write } = dispatch; const loc = dispatch.fn.loc; - // The inherited EventTarget/Event members of a subclass instance: the - // runtime's capsule view answers the accessors and method values. - if (eventTargetInheritedMember(info, name)) { + // The inherited EventTarget/Event members of a subclass instance, and + // the inherited Date methods of an instance of a class declared + // `extends Date`: the runtime's capsule view answers the accessors and + // method values. + if (eventTargetInheritedMember(info, name) || dateInheritedMember(lowerer, info, name)) { const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc }; if (!write) return [ diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index cc8700469d..abbaf3a754 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -1,3 +1,4 @@ +import { isErrorPrototypeValue } from "./class-prototypes.js"; import * as ts from "../ts7/adapter.js"; import { literalValues } from "../literal-values.js"; import { isJsSourceFile } from "../program.js"; @@ -377,6 +378,8 @@ export function collectClassMethodMutations( node.initializer.kind !== ts.SyntaxKind.MissingDeclaration ) markTarget(node.initializer, node); + if (ts.isPropertyAccessExpression(node) && isErrorPrototypeValue(lowerer, node)) + lowerer.errorPrototypeValues = true; if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) { const base = unwrap(node.expression); const prototype = diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 4cbd8e5cff..2a05cf9773 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -457,6 +457,28 @@ export function isCompiledPrototypeMember( ); } +/** `Error.prototype` read as the checked-dynamic object, as a value or for + * a member other than `toString` and `constructor`, which keep the Error + * class's prototype data (`this.toString !== Error.prototype.toString` + * compares class method values). */ +export function isErrorPrototypeValue( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): boolean { + let node: ts.Node = expr; + while (ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + return ( + expr.name.text === "prototype" && + lowerer.isStdlibGlobal(expr.expression, "Error") && + !( + ts.isPropertyAccessExpression(parent) && + parent.expression === node && + (parent.name.text === "toString" || parent.name.text === "constructor") + ) + ); +} + /** Named data and method slots have native descriptors. Bare reflection and * accessor replacement still require a complete view of the prototype. */ export function lowerClassPrototypeData( @@ -469,6 +491,11 @@ export function lowerClassPrototypeData( exactClassOfReceiver(lowerer, expr.expression) ?? (stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined); if (!info) return null; + // Error.prototype as a value: the checked-dynamic object an ES5 + // constructor function inherits from (`F.prototype = + // Object.create(Error.prototype)`). + if (isErrorPrototypeValue(lowerer, expr)) + return { kind: "libCall", fn: "dyn.errorPrototype", args: [], type: DYN, loc: locOf(expr) }; // EventEmitter.prototype and the stream prototypes: the untyped // constructor's prototype object (builtin-class-dyn.ts). const builtinKind = builtinClassKind(info.def.name); diff --git a/packages/compiler/src/frontend/lowering/class-to-primitive.ts b/packages/compiler/src/frontend/lowering/class-to-primitive.ts index f3a6b23fa9..0b107ab4ed 100644 --- a/packages/compiler/src/frontend/lowering/class-to-primitive.ts +++ b/packages/compiler/src/frontend/lowering/class-to-primitive.ts @@ -1,10 +1,21 @@ import { varRef } from "../../ir/build.js"; -import { BOOL, DYN, F64, STRING, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; +import { + BOOL, + DATE_T, + DYN, + F64, + STRING, + type IrExpr, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js"; import { isClassCallback } from "./class-callbacks.js"; import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; +import { DATE_TIME_FIELD, dateRootOf } from "./builtins/date-classes.js"; +import { dateValueMethod } from "./builtins/dates.js"; /** The ToPrimitive hint of a conversion: `String(x)` and template literals * take "string", `+` "default", `Number(x)` "number". */ @@ -13,9 +24,12 @@ export type StringConversionHint = "string" | "default"; type ConversionMethod = "sym:toPrimitive" | "valueOf" | "toString"; -/** How an instance of one class converts: through one of its methods, or - * through Object.prototype.toString ("[object Object]"). */ -type Plan = { method: ConversionMethod; ret: IrType } | { method: null }; +/** How an instance of one class converts: through one of its methods, + * through the Date.prototype method `date` of a class declared `extends + * Date`, or through Object.prototype.toString ("[object Object]"). */ +type Plan = + | { method: ConversionMethod; ret: IrType } + | { method: null; date?: "toString" | "valueOf" }; const PRIMITIVE_KINDS = new Set(["f64", "string", "bool", "bigint", "undefinedT", "nullT"]); @@ -138,6 +152,18 @@ function classConversion( } const convert = (cls: ClassInfo, object: IrExpr): IrExpr => { const plan = plans.get(cls)!; + if (plan.method === null && plan.date) { + const root = dateRootOf(cls)!; + const time: IrExpr = { + kind: "fieldGet", + obj: lowerer.upcastTo(object, root.def.name), + className: root.def.name, + field: DATE_TIME_FIELD, + type: DATE_T, + loc, + }; + return finish(dateValueMethod(plan.date, time, loc)!); + } if (plan.method === null) return finish({ kind: "strLit", value: "[object Object]", type: STRING, loc }); const args: IrExpr[] = @@ -202,7 +228,10 @@ function depth(info: ClassInfo): number { } function samePlan(a: Plan, b: Plan): boolean { - return a.method === b.method; + return ( + a.method === b.method && + (a.method !== null || b.method !== null || a.date === (b as { date?: string }).date) + ); } /** A conversion method the class declares or inherits (a field of that @@ -227,11 +256,17 @@ function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: t // OrdinaryToPrimitive: "string" tries toString first, which every object // has (Object.prototype.toString answers a string); "default" and // "number" try valueOf first, whose Object.prototype version answers the - // object itself. - const order: ConversionMethod[] = hint === "string" ? ["toString"] : ["valueOf", "toString"]; + // object itself. Date.prototype[Symbol.toPrimitive] takes "default" as + // "string", and Date.prototype answers both methods with primitives. + const date = dateRootOf(info) !== null; + const order: ConversionMethod[] = + hint === "string" || (date && hint === "default") ? ["toString"] : ["valueOf", "toString"]; for (const method of order) { const found = declared(lowerer, info, method); - if (!found) continue; + if (!found) { + if (date && method !== "sym:toPrimitive") return { method: null, date: method }; + continue; + } if (!primitive(lowerer, found.ret)) refuse(lowerer, node, info, `a '${method}' method that can return an object`); return { method, ret: found.ret }; diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index fafbbf695a..ead313a781 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -902,10 +902,15 @@ export function lowerCompoundValueToTarget( // operator on both values, then the checked conversion into the // target's declared type (a string result for a number target throws the // catchable TypeError, as the expanded assignment does). + // A union operand (`fraction += "0"` on a binding the program assigns + // both a number and a string) takes the same runtime operator. if ( - (target.type.kind === "dyn" || rhs.type.kind === "dyn") && - (isJsSourceFile(ts.getSourceFileOfNode(expr)) || - (lowerer.dynConvertible(read.type) && lowerer.dynConvertible(rhs.type))) + ((target.type.kind === "dyn" || rhs.type.kind === "dyn") && + (isJsSourceFile(ts.getSourceFileOfNode(expr)) || + (lowerer.dynConvertible(read.type) && lowerer.dynConvertible(rhs.type)))) || + ((target.type.kind === "union" || rhs.type.kind === "union") && + lowerer.dynConvertible(read.type) && + lowerer.dynConvertible(rhs.type)) ) { const left = lowerer.coerceInto(expr.left, read, DYN); const right = lowerer.coerceInto(expr.right, rhs, DYN); @@ -1276,6 +1281,48 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr loc, }; } + // `a[k] = b[k] = v` in a JavaScript file: a member write in value + // position lowers as its statement form and yields the value. The + // receiver, the key and the value are evaluated in that order into + // temporaries first. A receiver held in a temporary must be a + // checked-dynamic value, which the temporary shares; any other receiver + // must be a variable or `this`. + if ( + op === ts.SyntaxKind.EqualsToken && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && + (ts.isPropertyAccessExpression(expr.left) || ts.isElementAccessExpression(expr.left)) && + !expr.left.questionDotToken + ) { + const target = expr.left; + const receiverNode = target.expression; + const bareReceiver = + ts.isIdentifier(receiverNode) || receiverNode.kind === ts.SyntaxKind.ThisKeyword; + if (bareReceiver || tryLowerExpression(lowerer, receiverNode)?.type.kind === "dyn") { + const stmts: IrStmt[] = []; + const overrides: { node: ts.Expression; value: IrExpr }[] = []; + const hold = (node: ts.Expression, name: string): IrExpr => { + const value = lowerer.lowerExpr(node); + const local = lowerer.declareHiddenLocal(name, value.type); + stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc }); + const held = varRef(local.id, value.type, value.loc); + overrides.push({ node, value: held }); + return held; + }; + if (!bareReceiver) hold(receiverNode, "%assignReceiver"); + if ( + ts.isElementAccessExpression(target) && + !ts.isIdentifier(target.argumentExpression) && + !ts.isStringLiteral(target.argumentExpression) && + !ts.isNumericLiteral(target.argumentExpression) + ) + hold(target.argumentExpression, "%assignKey"); + const result = hold(expr.right, "%assignValue"); + stmts.push( + lowerer.withExpressionOverrides(overrides, () => lowerer.lowerExprStatement(expr)), + ); + return { kind: "seqExpr", stmts, result, type: result.type, loc }; + } + } lowerer.unsupported( "SC1090", expr, diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 74f0be9555..3ca1471aec 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -616,9 +616,25 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre // An indexed string property is absent for non-integer and missing // indexes. charAt has different coercion and out-of-range semantics. const recv = lowerer.lowerExpr(expr.expression); + const rawIndex = lowerer.lowerExpr(expr.argumentExpression); + // A checked-dynamic index (an untyped JavaScript parameter, a parsed + // value) reads the property its key names, as JavaScript does: a code + // unit for an integer index, the length for "length", else undefined. + // The checker's string type does not describe those results, so the + // value stays checked-dynamic and consumers convert it. + if (rawIndex.type.kind === "dyn" && lowerer.dynConvertible(recv.type)) { + const key = lowerRecordPropertyKey(lowerer, rawIndex, expr.argumentExpression); + return { + kind: "dynKeyGet", + value: lowerer.coerceInto(expr.expression, recv, DYN), + key, + type: DYN, + loc: locOf(expr), + }; + } const index = lowerOptionalNumber( lowerer, - lowerer.lowerExpr(expr.argumentExpression), + rawIndex, locOf(expr.argumentExpression), expr.argumentExpression, ); diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 565db0cd8d..e4f7d8675b 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -797,6 +797,7 @@ export function lowerObjectLiteral( ): IrExpr { // Unannotated JavaScript accumulator fields have no element contract. // Preserve their checked arrays instead of assigning never[] a numeric ABI. + // An empty object field is a map the program adds keys to. if ( !expected && isJsSourceFile(ts.getSourceFileOfNode(expr)) && @@ -805,6 +806,8 @@ export function lowerObjectLiteral( ts.isPropertyAssignment(prop) && ((ts.isArrayLiteralExpression(prop.initializer) && prop.initializer.elements.nodes.length === 0) || + (ts.isObjectLiteralExpression(prop.initializer) && + prop.initializer.properties.nodes.length === 0) || neverTaintedJsType(lowerer, prop.initializer, lowerer.typeOf(prop.initializer))), ) ) { diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index f0b980da05..895ff5f458 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -5,7 +5,11 @@ import { lowerHistogramProperty } from "../builtins/performance.js"; import { staticFunctionNameRead } from "../static-function-names.js"; import { typedArgumentsArity } from "../typed-arguments.js"; import { isThisParameter } from "../call-signatures.js"; -import { localPrivateBrandClass } from "../private-brands.js"; +import { + checkedPrivateValue, + localPrivateBrandClass, + privateNameClass, +} from "../private-brands.js"; import { type Lowerer, PoisonError, jsFuncNameOf, neverTaintedJsType, own } from "../lowerer.js"; import { ARRAY_METHODS, @@ -90,7 +94,7 @@ import { runtimeConstructorReceiver, } from "../builtins/runtime-constructor.js"; import { findGenericMethodOn, lowerStaticFieldRead } from "../lower-classes.js"; -import { lowerClassPrototypeData } from "../class-prototypes.js"; +import { isErrorPrototypeValue, lowerClassPrototypeData } from "../class-prototypes.js"; import { lowerEnumAccess } from "../lower-enums.js"; import { hasOptionalChainGuard, @@ -281,6 +285,24 @@ export function lowerPropertyAccessExpr( expr: ts.PropertyAccessExpression, loc: SrcLoc, ): IrExpr { + // `value.#x` on a checked-dynamic value, or on a checked extraction of + // one: the field of the brand-checked receiver (checkedPrivateValue). + if (ts.isPrivateIdentifier(expr.name)) { + const owner = privateNameClass(lowerer, expr.name); + const field = owner?.fields.get(expr.name.text); + const probed = field ? tryLowerExpression(lowerer, expr.expression) : null; + if (owner && field && probed && checkedDynamicValue(probed)) { + const value = checkedDynamicValue(lowerer.lowerExpr(expr.expression))!; + return { + kind: "fieldGet", + obj: checkedPrivateValue(lowerer, owner, expr, value, loc), + className: owner.def.name, + field: expr.name.text, + type: field, + loc, + }; + } + } if (!expr.questionDotToken) { const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); if (navigator) return navigator; @@ -1664,6 +1686,19 @@ export function lowerIntrinsicProperty( return { kind: "libCall", fn: "dyn.arrayPrototype", args: [], type: DYN, loc: locOf(expr) }; if (lowerer.stdlibGlobalMember(expr, "Object") === "prototype") return { kind: "libCall", fn: "dyn.objectPrototype", args: [], type: DYN, loc: locOf(expr) }; + // Error.prototype as a value (`F.prototype = Object.create(Error.prototype)`) + // and Error.captureStackTrace read as a function (`if + // (Error.captureStackTrace) ...`). + if (isErrorPrototypeValue(lowerer, expr)) + return { kind: "libCall", fn: "dyn.errorPrototype", args: [], type: DYN, loc: locOf(expr) }; + if (lowerer.stdlibGlobalMember(expr, "Error") === "captureStackTrace") + return { + kind: "libCall", + fn: "dyn.errorCaptureStackTraceFunction", + args: [], + type: DYN, + loc: locOf(expr), + }; // Function.prototype as a VALUE (a prototype-chain link: // `Object.setPrototypeOf(F.prototype, Function.prototype)`): one // anonymous function answering undefined, length 0 — a zero-capture @@ -3533,6 +3568,13 @@ export function classFieldTarget( * known class instance OR a record, and the member is a field or (class * receivers) a declared accessor property. Returns the pieces of a * fieldSet/recordSet/accessor-call (minus value/kind) or null. */ +/** The checked-dynamic value `expr` reads: itself, or the value a checked + * extraction validates. Null for other expressions. */ +function checkedDynamicValue(expr: IrExpr): IrExpr | null { + if (expr.type.kind === "dyn") return expr; + return expr.kind === "dynCheck" && expr.value.type.kind === "dyn" ? expr.value : null; +} + export function fieldTarget( lowerer: Lowerer, access: ts.PropertyAccessExpression, @@ -3569,6 +3611,30 @@ export function fieldTarget( return classFieldTarget(lowerer, access.expression, ownerType, access.name.text); } } + // `value.#x = v` on a checked-dynamic value: the field of the + // brand-checked receiver (checkedPrivateValue). + if (ts.isPrivateIdentifier(access.name)) { + const owner = privateNameClass(lowerer, access.name); + const probed = owner?.fields.has(access.name.text) + ? tryLowerExpression(lowerer, access.expression) + : null; + if (owner && probed && checkedDynamicValue(probed)) { + const receiver = checkedPrivateValue( + lowerer, + owner, + access, + checkedDynamicValue(lowerer.lowerExpr(access.expression))!, + locOf(access), + ); + return classFieldTarget( + lowerer, + access.expression, + { kind: "object", className: owner.def.name }, + access.name.text, + receiver, + ); + } + } const stored = ts.isIdentifier(access.expression) ? (lowerer.peekLocal(access.expression)?.type ?? lowerer.globalOf(access.expression)?.type) : undefined; diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 088b45cffa..37478be0d1 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -1,4 +1,5 @@ /* instanceof and in, including private-name and runtime-key in. */ +import { dateRootOf } from "../builtins/date-classes.js"; import * as ts from "../../ts7/adapter.js"; import { builtinClassInstanceOfAny, builtinClassKind } from "../builtins/builtin-class-dyn.js"; import { @@ -265,7 +266,12 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc stmts: [{ kind: "exprStmt", expr: value, loc }], result: { kind: "boolLit", - value: value.type.kind === (date ? "date" : "url"), + value: + value.type.kind === (date ? "date" : "url") || + // An instance of a class declared `extends Date`. + (date && + value.type.kind === "object" && + dateRootOf(lowerer.classes.get(value.type.className)) !== null), type: BOOL, loc, }, @@ -565,7 +571,33 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc loc, }; }; - const test = caughtInstanceOf(target); + let test = caughtInstanceOf(target); + // A thrown checked-dynamic object whose prototype chain reaches + // Error.prototype (an ES5 constructor function inheriting from it). + if (target.def.name === "%Error" && lowerer.errorPrototypeValues) { + const value = varRef(caughtLocal.id, CAUGHT, loc); + test = { + kind: "logical", + op: "||", + left: test, + right: { + kind: "logical", + op: "&&", + left: { kind: "caughtTest", value, test: "dynamic", type: BOOL, loc }, + right: { + kind: "libCall", + fn: "dyn.inheritsErrorPrototype", + args: [{ kind: "caughtToDyn", value, type: DYN, loc }], + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } return duplex ? { kind: "logical", op: "||", left: test, right: caughtInstanceOf(duplex), type: BOOL, loc } : test; @@ -590,9 +622,35 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc ) { return classInstanceOf(lowerer, left, target, loc); } - if (left.type.kind === "dyn" && target.def.name === "%Error") { + if (left.type.kind === "dyn" && target.def.name === "%Error" && !lowerer.errorPrototypeValues) { return { kind: "dynTest", test: "error", value: left, type: BOOL, loc }; } + // An object whose prototype chain reaches Error.prototype (an ES5 + // constructor function inheriting from it) is an instance as well. + if (left.type.kind === "dyn" && target.def.name === "%Error") { + const local = lowerer.declareHiddenLocal("%instanceofValue", DYN); + const value = varRef(local.id, DYN, loc); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: left, loc }], + result: { + kind: "logical", + op: "||", + left: { kind: "dynTest", test: "error", value, type: BOOL, loc }, + right: { + kind: "libCall", + fn: "dyn.inheritsErrorPrototype", + args: [value], + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } // `u instanceof TypeError` (and the other BUILTIN error classes) on a // dyn value: the from_error cache holds the checked-dynamic tree↔error identity edge, // so the runtime resolves the encoding back to its error and asks the diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index 13baca7d20..128093dad8 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -23,6 +23,7 @@ import { type IrExpr, type IrStmt, type IrType, + isUnitType, STRING, type SrcLoc, UNDEFINED_T, @@ -100,6 +101,27 @@ function writeFree(expr: IrExpr): boolean { } } +/** A union unary `+`/`-` converts with the runtime's ToNumber: one with an + * arm other than numbers and units (`string | undefined`). Number and unit + * unions keep their native lowering. */ +function coercedUnion(lowerer: Lowerer, type: IrType): boolean { + if (type.kind !== "union" || !lowerer.dynConvertible(type)) return false; + const arms = lowerer.unions.get(type.unionId)?.arms ?? []; + return arms.some((arm) => arm.kind !== "f64" && !isUnitType(arm)); +} + +/** ToNumber of a boolean: 1 for true, 0 for false. */ +function booleanToNumber(value: IrExpr, loc: SrcLoc): IrExpr { + return { + kind: "ternary", + cond: value, + then: { kind: "numLit", value: 1, type: F64, loc }, + else_: { kind: "numLit", value: 0, type: F64, loc }, + type: F64, + loc, + }; +} + export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpression): IrExpr { const loc = locOf(expr); switch (expr.operator) { @@ -111,9 +133,25 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio } // Match the checked numeric boundary used by JS binary arithmetic. const operand = - raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)) - ? { kind: "dynCheck" as const, value: raw, type: F64, loc } - : lowerOptionalNumber(lowerer, raw, loc, expr.operand); + raw.type.kind === "bool" + ? booleanToNumber(raw, loc) + : coercedUnion(lowerer, raw.type) + ? ({ + kind: "libCall", + fn: "dyn.toNumberCoerce", + args: [{ kind: "dynFrom", value: raw, type: DYN, loc }], + type: F64, + loc, + } satisfies IrExpr) + : raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)) + ? ({ + kind: "libCall", + fn: "dyn.toNumberCoerce", + args: [raw], + type: F64, + loc, + } satisfies IrExpr) + : lowerOptionalNumber(lowerer, raw, loc, expr.operand); if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "-", [raw.type]); if (operand.kind === "numLit") return { ...operand, value: -operand.value, loc }; return { kind: "unary", op: "-", operand, type: F64, loc }; @@ -123,6 +161,12 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio if (raw.type.kind === "dyn") { return { kind: "libCall", fn: "dyn.toNumberCoerce", args: [raw], type: F64, loc }; } + // A union of primitives (`+match[1]` over `string | undefined`): ToNumber + // over the boxed value, as for a checked-dynamic operand. + if (coercedUnion(lowerer, raw.type)) { + const boxed: IrExpr = { kind: "dynFrom", value: raw, type: DYN, loc }; + return { kind: "libCall", fn: "dyn.toNumberCoerce", args: [boxed], type: F64, loc }; + } // Unary + is ToNumber; on an already-number operand it's identity, // and a STRING operand runs the runtime's ECMA-exact StringToNumber // (num.fromString — Number(aString)'s lowering, scr_string.c). @@ -130,6 +174,7 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio if (raw.type.kind === "string") { return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc }; } + if (raw.type.kind === "bool") return booleanToNumber(raw, loc); const classNumber = classToNumberConversion(lowerer, raw, expr.operand, "unary plus"); if (classNumber) return classNumber; const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand); @@ -321,9 +366,13 @@ export function lowerIncDec( !isJsSourceFile(ts.getSourceFileOfNode(expr)) && raw.type.kind === "dyn" && lowerer.mapTypeOf(lowerer.typeOf(access))?.kind === "f64"; + // A private name is the field of its class (fieldTarget). + const privateName = + ts.isPropertyAccessExpression(access) && ts.isPrivateIdentifier(access.name); if ( (isJsSourceFile(ts.getSourceFileOfNode(expr)) || checkedNumber) && - raw.type.kind === "dyn" + raw.type.kind === "dyn" && + !privateName ) { const receiver = save(raw, "%incrementReceiver"); const key = save( @@ -814,7 +863,8 @@ export function lowerFieldCompoundValue( probed?.type.kind === "classval" ? lowerer.classes.get(probed.type.className) : undefined; if (probed && staticClass && hasRuntimeStatics(staticClass)) probed = lowerer.coerceToExpected(probed, DYN); - if (probed?.type.kind === "dyn") { + // A private name is the field of its class (fieldTarget). + if (probed?.type.kind === "dyn" && !ts.isPrivateIdentifier(access.name)) { const receiver = save(probed, "%compoundReceiver"); const key: IrExpr = { kind: "strLit", diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 7102f37ab1..21b974e31f 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -141,7 +141,7 @@ import { lowerCryptoHashMethodCall, lowerPromisifiedCryptoCall, } from "./builtins/crypto.js"; -import { lowerErrorIsErrorCall } from "./builtins/errors.js"; +import { lowerErrorCaptureStackTraceCall, lowerErrorIsErrorCall } from "./builtins/errors.js"; import { lowerPromisifiedDnsCall } from "./lower-dgram.js"; import { lowerDisposableStackUseCall } from "./builtins/disposable-stack.js"; import { @@ -199,6 +199,11 @@ import { import { classCallbackCall, isClassCallback } from "./class-callbacks.js"; import { emitterRooted, lowerEmitterMethodCall } from "./lower-event-emitter.js"; import { eventTargetApiMethod, lowerEventTargetMethodCall } from "./lower-event-target.js"; +import { + dateRootOf, + isDatePrototypeMethod, + lowerInheritedDateMethodCall, +} from "./builtins/date-classes.js"; import { lowerConsoleInstanceCall, lowerConsoleMethodCall, @@ -1554,6 +1559,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.lowerFilterNarrowCall(expr, expr.expression) ?? lowerArrayIsArrayCall(lowerer, expr, expr.expression) ?? lowerErrorIsErrorCall(lowerer, expr, expr.expression) ?? + lowerErrorCaptureStackTraceCall(lowerer, expr, expr.expression) ?? lowerBigIntMethodCall(lowerer, expr, expr.expression) ?? lowerSymbolStaticCall(lowerer, expr, expr.expression) ?? lowerSymbolMethodCall(lowerer, expr, expr.expression) ?? @@ -3727,6 +3733,18 @@ function lowerDynReceiverMethodCall( // IS Node's semantics for every possible dyn value. Shared names would // need a receiver-kind dispatch — they keep the fence. if (DYN_STRING_ONLY_METHODS.has(access.name.text) && !ownCallable) { + // An object-typed receiver the member is missing from (a JavaScript + // module object whose members are assigned in a function body, + // `common.repeat(" ", n)`) holds a string or an object with that + // member: dispatch on the receiver's runtime kind. + if ( + !checkerUntyped && + (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 && + !call.questionDotToken && + !access.questionDotToken && + !call.arguments.nodes.some(ts.isSpreadElement) + ) + return lowerStringOrMemberCall(lowerer, call, access, recv); const checked = (): IrExpr => dynStringReceiver(lowerer, recv, access); return ( lowerRegexMethodCall(lowerer, call, access, checked) ?? @@ -3935,6 +3953,98 @@ function lowerDynReceiverMethodCall( }; } +/** `value.name(...args)` on a dyn receiver that may be a string or an + * object: a string runs the String.prototype lowering, any other value + * calls its member. The member is read before the arguments are + * evaluated, and the arguments are evaluated once for either branch. */ +function lowerStringOrMemberCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, + recv: IrExpr, +): IrExpr { + const loc = locOf(call); + const receiverLocal = lowerer.declareHiddenLocal("%dynCallReceiver", DYN); + const receiver = varRef(receiverLocal.id, DYN, loc); + const isString: IrExpr = { kind: "dynTest", test: "string", value: receiver, type: BOOL, loc }; + const calleeLocal = lowerer.declareHiddenLocal("%dynCallCallee", DYN); + const callee = varRef(calleeLocal.id, DYN, loc); + const stmts: IrStmt[] = [ + { kind: "varDecl", localId: receiverLocal.id, init: recv, loc }, + { + kind: "varDecl", + localId: calleeLocal.id, + init: { + kind: "ternary", + cond: isString, + then: dynUndefinedExpr(loc), + else_: { + kind: "dynKeyGet", + key: { kind: "strLit", value: access.name.text, type: STRING, loc: locOf(access) }, + value: receiver, + type: DYN, + loc: locOf(access), + }, + type: DYN, + loc, + }, + loc, + }, + ]; + const args = call.arguments.nodes.map((node) => { + const value = lowerer.lowerExpr(node); + const local = lowerer.declareHiddenLocal("%dynCallArg", value.type); + stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc }); + return { node, value: varRef(local.id, value.type, value.loc) }; + }); + // String.prototype methods ignore arguments past the ones they read. + const arity = DYN_STRING_ONLY_ARITY[access.name.text] ?? call.arguments.nodes.length; + const stringReceiver = (): IrExpr => ({ kind: "dynCheck", value: receiver, type: STRING, loc }); + const lowerString = (): IrExpr | null => + (call.arguments.nodes.length <= arity + ? lowerRegexMethodCall(lowerer, call, access, stringReceiver) + : null) ?? + lowerStringMethodCall( + lowerer, + call, + access, + stringReceiver, + call.arguments.nodes.slice(0, arity), + ); + const stringCall = lowerer.withExpressionOverrides(args, lowerString); + const memberCall: IrExpr = { + kind: "dynCall", + callee, + receiver, + calleeName: ts.getTextOfNode(access), + args: args.map((arg) => lowerer.coerceToExpected(arg.value, DYN)), + type: DYN, + loc, + }; + return { + kind: "seqExpr", + stmts, + result: { + kind: "ternary", + cond: isString, + then: stringCall + ? lowerer.coerceToExpected(stringCall, DYN) + : nodeThrowExpr( + 0, + "SC1090", + `method calls like '${ts.getTextOfNode(access)}' on a string are not supported yet [SC1090 at ${ts.getSourceFileOfNode(call).fileName}:${ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(call), ts.tokenStartOfNode(call)).line + 1}]`, + DYN, + loc, + ), + else_: memberCall, + type: DYN, + loc, + }, + type: DYN, + loc, + }; +} + /** Prototype method names of the checked-dynamic tree-representable kinds (String, Array, * Object, Function, Number prototypes): a dyn receiver call on one of * these could be a REAL method on a real value, which a stored-member @@ -4372,6 +4482,29 @@ function isStringMethodName(name: string): boolean { ); } +/** How many arguments each DYN_STRING_ONLY_METHODS method reads. */ +const DYN_STRING_ONLY_ARITY: Readonly> = { + charCodeAt: 1, + charAt: 1, + startsWith: 2, + endsWith: 2, + substring: 2, + repeat: 1, + trim: 0, + trimStart: 0, + trimEnd: 0, + split: 2, + padStart: 2, + padEnd: 2, + toLowerCase: 0, + toUpperCase: 0, + replace: 2, + replaceAll: 2, + match: 1, + matchAll: 1, + search: 1, +}; + /** STR_METHODS ∪ the regex-form names, MINUS everything Array (or any * other dyn kind's prototype) also declares. */ const DYN_STRING_ONLY_METHODS = new Set([ @@ -9670,6 +9803,10 @@ export function lowerObjectMethodCall( if (info && !found && eventTargetApiMethod(info, access.name.text)) { return lowerEventTargetMethodCall(lowerer, call, access, info, lowerReceiver()); } + // The Date methods a class declared `extends Date` does not declare. + if (info && !found && dateRootOf(info) && isDatePrototypeMethod(access.name.text)) { + return lowerInheritedDateMethodCall(lowerer, call, access.name.text, info, lowerReceiver()); + } // GENERIC methods (own type parameters) never enter the methods table: // they monomorphize per call site and dispatch statically // (lowerClassGenericMethodCall has the exactness rules). @@ -10458,6 +10595,17 @@ export function lowerStaticNumberGlobal( : lowerer.lowerExprExpecting(args[0]!, STRING); return { kind: "libCall", fn: "num.parseInt", args: [s, radix], type: F64, loc }; } + // parseFloat takes one argument; Node ignores the rest after evaluating + // them (`parseFloat(text, 10)`). Literal and identifier extras have + // nothing to evaluate. + if ( + name === "parseFloat" && + args.length > 1 && + args + .slice(1) + .every((arg) => ts.isNumericLiteral(arg) || ts.isStringLiteral(arg) || ts.isIdentifier(arg)) + ) + return lowerStaticNumberGlobal(lowerer, name, call, args.slice(0, 1), loc); // STATIC parseFloat over string arguments — parseInt's sibling // (num.parseFloat is ECMA 19.2.4's decimal-literal prefix parse in // scr_string.c). Other argument types retain their native coercion diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 790efe988d..9976a37e45 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,3 +1,11 @@ +import { + DATE_TIME_FIELD, + dateRootOf, + dateSuperCall, + isDatePrototypeMethod, + lowerDateSuperMethodCall, +} from "./builtins/date-classes.js"; +import { lowerDateStaticCall } from "./builtins/dates.js"; import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; import { adaptPromiseExecutor } from "./promise-executor-adapter.js"; import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; @@ -564,6 +572,11 @@ export interface ClassInfo { * lower-event-target.ts over any class rooted here. Subclass structs * embed the runtime prefix. */ builtinEventTarget?: "target" | "event"; + /** A class declared `extends Date` (builtins/date-classes.ts): its first + * field is the hidden `%time` value, super() sets it with the Date + * constructor's rules, and the Date methods it does not declare lower + * over that value. Subclasses inherit the field. */ + extendsDate?: true; /** The synthetic LifetimeRegion class (@scriptc/experimental, * lifetime-regions.ts): its constructor is synthesized with the class, * and its methods lower at their call sites. */ @@ -1951,6 +1964,7 @@ export function collectClassShapeInner( : (computedBase?.classInfo ?? null); let genericBase: ts.ExpressionWithTypeArguments | undefined; let callableBase: ClassInfo["callableBase"]; + let extendsDate = false; let factoryBaseExpression: ts.Expression | undefined; const adoptCallableBase = (expression: ts.Expression): boolean => { if (familyMode || inst || mixin) return false; @@ -2151,6 +2165,11 @@ export function collectClassShapeInner( if (t && adoptCallableBase(t.expression)) continue; lowerer.unsupported("SC1090", clause, "extending computed expressions"); } + if (!familyMode && !inst && !mixin && lowerer.isStdlibGlobal(t.expression, "Date")) { + if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes"); + extendsDate = true; + continue; + } const symbol = lowerer.resolveValueSymbol(t.expression); // Extending a REBINDABLE decorated class (analysis already ran — // this collection is a class expression or a generic @@ -4350,7 +4369,8 @@ export function collectClassShapeInner( const ctorParams: ParamShape[] = ctor && !mixinForwarding ? lowerer.paramShapes(ctor.parameters.nodes) - : (base?.ctorParams ?? (callableBase ? [{ type: DYN, mode: "dynRest" }] : [])); + : (base?.ctorParams ?? + (callableBase || extendsDate ? [{ type: DYN, mode: "dynRest" }] : [])); if ( ctor && !mixinForwarding && @@ -4388,6 +4408,7 @@ export function collectClassShapeInner( // Redeclared INHERITED fields contribute no slot (their // initializers assign the prefix slot). fields: [ + ...(extendsDate ? [{ name: DATE_TIME_FIELD, type: DATE_T }] : []), ...(base?.def.fields ?? []), ...fieldOrder .filter((f) => f.redeclared !== true) @@ -4419,6 +4440,7 @@ export function collectClassShapeInner( base, ...(genericBase ? { genericBase } : {}), ...(callableBase ? { callableBase } : {}), + ...(extendsDate ? { extendsDate: true as const } : {}), ...(factoryBaseExpression ? { factoryBaseExpression } : {}), subclasses: [], throwingSetters, @@ -5647,6 +5669,15 @@ export function lowerStaticFieldRead( if (info.classDecorators?.valueGlobalId !== undefined) return null; const loc = locOf(expr); const found = findStaticOn(lowerer, info, expr.name.text); + // A Date static read as a value through a class declared `extends Date`: + // the statics lower only as calls. + if ( + !found && + dateRootOf(info) && + ["now", "parse", "UTC"].includes(expr.name.text) && + !(ts.isCallExpression(expr.parent) && expr.parent.expression === expr) + ) + lowerer.noLowering(`Date.${expr.name.text} read through a class that extends Date`, expr); if (hasRuntimeStatics(info) && !found?.field) return { kind: "dynKeyGet", @@ -6422,6 +6453,12 @@ function staticCallOn( const loc = locOf(call); const runtimeStatic = directRuntimeStaticCall(lowerer, call, access, info, throughValue); if (runtimeStatic) return runtimeStatic; + // Date's statics through a class declared `extends Date` + // (`Moment.UTC(...)`), which inherits them. + if (dateRootOf(info) && !findStaticOn(lowerer, info, access.name.text)) { + const date = lowerDateStaticCall(lowerer, call, access.name.text); + if (date) return date; + } if ( (hasRuntimeStatics(info) && !findStaticOn(lowerer, info, access.name.text)?.field) || (info.callableBase && @@ -7683,7 +7720,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { lowerer.ctx.argumentsLocal = local; } body.push(...declared.prologue); - if (!ctorBase && !info.callableBase) { + if (!ctorBase && !info.callableBase && !info.extendsDate) { // Node's base-class order: field initializers run at the start // of construction, the parameter-property assignments open the // constructor body (probed — a field initializer reading a @@ -7695,7 +7732,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { body.push(...lowerer.lowerDerivedCtorBody(info, thisLocal)); } } else { - if (ctorBase || info.callableBase) { + if (ctorBase || info.callableBase || info.extendsDate) { // Synthetic forwarding params (the inherited ABI signature). // Nothing references them by symbol — only the super call below, // which forwards the already-completed values UNCHANGED (defaults @@ -8614,12 +8651,14 @@ export function lowerDerivedCtorBody( loc, }, ]; - if (info.callableBase) { + if (info.callableBase || info.extendsDate) { if (superCall.arguments.nodes.some(ts.isSpreadElement)) lowerer.unsupported( "SC1090", superCall, - "spreading explicit super arguments into an ordinary constructor", + info.extendsDate + ? "spreading super arguments into the Date constructor" + : "spreading explicit super arguments into an ordinary constructor", ); const pack: IrExpr = { kind: "dynArrLit", @@ -8804,6 +8843,7 @@ export function superCallStmt( type: { kind: "object", className: info.def.name }, loc, }; + if (info.extendsDate) return dateSuperCall(thisRef, info, args[0]!, loc); if (info.callableBase) return { kind: "exprStmt", @@ -8925,6 +8965,24 @@ export function lowerSuperMethodCall( access: ts.PropertyAccessExpression, ): IrExpr { const cls = lowerer.currentClass; + // `super.toISOString()` in a class declared `extends Date`. + if (cls?.extendsDate && !cls.base) { + const thisL = lowerer.resolveThis(); + if (!thisL) lowerer.unsupported("SC1080", access); + const thisRef: IrExpr = { + kind: "varRef", + localId: thisL.id, + type: thisL.type, + loc: locOf(call), + }; + const date = lowerDateSuperMethodCall(lowerer, call, access.name.text, cls, thisRef); + if (date) return date; + lowerer.unsupported( + "SC1090", + access, + `'super.${access.name.text}' (Date.prototype does not declare it)`, + ); + } if (!cls?.base) { // tsc rejects super outside derived-class bodies first; defensive. lowerer.unsupported("SC1090", access, "'super' outside a derived class"); @@ -8969,6 +9027,17 @@ export function lowerSuperMethodCall( // `super.dispatchEvent(e)`, `super.preventDefault()`: the inherited // runtime members over this method's own `this` (subclasses cannot // override them, so the static answer is the runtime's). + if (dateRootOf(cls.base) && isDatePrototypeMethod(access.name.text)) { + const thisL = lowerer.resolveThis(); + if (!thisL) lowerer.unsupported("SC1080", access); + const thisRef: IrExpr = { + kind: "varRef", + localId: thisL.id, + type: thisL.type, + loc: locOf(call), + }; + return lowerDateSuperMethodCall(lowerer, call, access.name.text, cls.base, thisRef)!; + } if (eventTargetApiMethod(cls.base, access.name.text)) { const thisL = lowerer.resolveThis(); if (!thisL) lowerer.unsupported("SC1080", access); diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 4be5d0b7ef..6f90b198c4 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -45,6 +45,7 @@ import type { ClassInfo } from "./lower-classes.js"; import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { symbolFieldDisplayName } from "./symbol-fields.js"; import { eventTargetRootOf } from "./lower-event-target.js"; +import { dateRootOf } from "./builtins/date-classes.js"; import { lowerSystemErrorMap } from "./lower-system-errors.js"; import { isPerfEntryType, perfEntryJson } from "./builtins/performance.js"; import { recordMayKeepSource, recordSourceTest } from "./record-sources.js"; @@ -804,8 +805,9 @@ function inspectSupport( const info = lowerer.classes.get(t.className); if (!info) return `class '${t.className}' has no inspect lowering`; // EventTarget and Event subclasses render through Node's inspectors - // for those classes (the runtime's capsule view). - if (eventTargetRootOf(info) !== null) return null; + // for those classes (the runtime's capsule view), and so do classes + // declared `extends Date`. + if (eventTargetRootOf(info) !== null || dateRootOf(info) !== null) return null; if (isErrorClass(lowerer, t.className)) { if (userErrorClasses(info).length > 0 && errorHeaderOverridden(lowerer, info)) return "error subclasses whose name, message or stack is an accessor or a prototype property have no inspect lowering yet"; @@ -957,7 +959,10 @@ export function inspectExpr( loc, }; case "object": - if (eventTargetRootOf(lowerer.classes.get(t.className)) !== null) { + if ( + eventTargetRootOf(lowerer.classes.get(t.className)) !== null || + dateRootOf(lowerer.classes.get(t.className)) !== null + ) { return { kind: "libCall", fn: "insp.dyn", diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index d1f276d44b..e12d90743b 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -148,6 +148,7 @@ import { forkTargetPaths } from "../fork-target.js"; import { workerTargetPaths } from "../worker-target.js"; import { isNativeProxyInitializer } from "./expressions/native-proxy.js"; import { handleEventReaderPrelude } from "./lower-event-target.js"; +import { dateClassOperationPrelude } from "./builtins/date-classes.js"; import { cjsAccessorSnapshotStores, jsHoldsDynObject } from "./lower-exprs.js"; import { keptIdentityAssertion } from "./assertion-identity.js"; @@ -3376,6 +3377,7 @@ export function buildMain(lowerer: Lowerer): IrFunction { ...lowerer.mainPrelude, ...cjsModuleRegistryPrelude(lowerer, loc), ...handleEventReaderPrelude(lowerer, loc), + ...dateClassOperationPrelude(lowerer, loc), ]; const roots = [lowerer.entry, ...lowerer.forkTargets, ...lowerer.workerTargets]; const isAsync = roots.some((root) => lowerer.asyncInitFiles.has(root)); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index e3480cf9c8..f1bf7c0411 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -6110,6 +6110,18 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar ) ) return true; + // A write that can store null or undefined into a binding the checker + // types as a primitive (`offset = match[3] || null`): JavaScript files + // are checked without strict null checks, so the declared type does not + // admit them. + const primitive = lowerer.mapTypeOf(lowerer.typeOf(decl.name)); + if ( + (primitive?.kind === "string" || primitive?.kind === "f64" || primitive?.kind === "bool") && + candidates.some( + (node) => node.operatorToken.kind === ts.SyntaxKind.EqualsToken && mayBeNullish(node.right), + ) + ) + return true; // A plain JavaScript write of a value the declared type cannot hold (the // checker does not report it): buffer-crc32 replaces its number[] table // with `new Int32Array(table)`. Only a different representation counts; @@ -6140,6 +6152,31 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar }); } +/** Whether `node` can evaluate to null or undefined by its form: the + * literals, `void`, and conditional or logical expressions with such an + * operand in result position. */ +function mayBeNullish(node: ts.Expression): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (node.kind === ts.SyntaxKind.NullKeyword || ts.isVoidExpression(node)) return true; + if (ts.isIdentifier(node)) return node.text === "undefined"; + if (ts.isConditionalExpression(node)) + return mayBeNullish(node.whenTrue) || mayBeNullish(node.whenFalse); + if (ts.isBinaryExpression(node)) { + const op = node.operatorToken.kind; + if ( + op === ts.SyntaxKind.BarBarToken || + op === ts.SyntaxKind.QuestionQuestionToken || + op === ts.SyntaxKind.AmpersandAmpersandToken + ) + return ( + mayBeNullish(node.right) || + (op === ts.SyntaxKind.AmpersandAmpersandToken && mayBeNullish(node.left)) + ); + if (op === ts.SyntaxKind.CommaToken) return mayBeNullish(node.right); + } + return false; +} + function immediatelyGuardedAbsenceProbe(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { const guard = immediateAbsenceGuard(lowerer, decl); // `if (!x)`: the falsy test sees a missing value. @@ -7560,8 +7597,9 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm // (closures box), the runtime sets the member on OBJ receivers // and throws Node's TypeErrors on the rest. The receiver is // PROBED (tryLowerExpression) and claimed exactly when it lowers to - // dyn; typed receivers keep their own lowerings and fences. - if (!expr.left.questionDotToken) { + // dyn; typed receivers keep their own lowerings and fences. A + // private name is the field of its class (fieldTarget). + if (!expr.left.questionDotToken && !ts.isPrivateIdentifier(expr.left.name)) { const recv = tryLowerExpression(lowerer, expr.left.expression); if (recv && recv.type.kind === "dyn") { const loc = locOf(expr); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ee8ba7ecba..e7ea878674 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -497,7 +497,8 @@ import { lowerTracingChannelProperty, } from "./builtins/async-context.js"; import { lowerDisposableStackNew } from "./builtins/disposable-stack.js"; -import { lowerErrorCodeProperty } from "./builtins/errors.js"; +import { lowerErrorCodeProperty, lowerErrorFunctionCall } from "./builtins/errors.js"; +import { noteDateClassMethods } from "./builtins/date-classes.js"; import { lowerProcessProperty, lowerProcessMethodCall, @@ -2944,6 +2945,10 @@ export class Lowerer { /** Named method slots observed before class collection need a callable * ABI and shared prototype descriptors instead of call-site specialization. */ readonly prototypeMethodAccesses = new Map(); + /** Whether the program reads `Error.prototype`, the checked-dynamic + * object an ES5 constructor function can inherit from: only then can a + * value that is not an Error instance be `instanceof Error`. */ + errorPrototypeValues = false; /** Well-known symbol methods (`sym:iterator`) the program may replace * through a symbol-keyed write: an instance or prototype property, or a * reflection call. */ @@ -8123,6 +8128,7 @@ export class Lowerer { // turn queue more instances. Continue to the joint fixpoint; the initial // queue above is the only portion whose discovery order differed from // historical emit order. + noteDateClassMethods(this); const classDispatch = new ClassDynamicDispatch(extraRoots !== undefined); // Units in declaration order, so the passes below meet functions, and the // classes they collect, in an order that does not depend on lowering order. @@ -12758,6 +12764,25 @@ export class Lowerer { ); } + /** withExpressionOverride for several nodes at once. */ + withExpressionOverrides( + overrides: readonly { node: ts.Expression; value: IrExpr }[], + lower: () => T, + ): T { + const previous = overrides.map((override) => this.chainRecvByNode.get(override.node)); + for (const override of overrides) this.chainRecvByNode.set(override.node, override.value); + try { + return lower(); + } finally { + for (let i = overrides.length - 1; i >= 0; i--) { + const node = overrides[i]!.node; + const value = previous[i]; + if (value) this.chainRecvByNode.set(node, value); + else this.chainRecvByNode.delete(node); + } + } + } + withExpressionOverride(node: ts.Expression, value: IrExpr, lower: () => T): T { const previous = this.chainRecvByNode.get(node); this.chainRecvByNode.set(node, value); @@ -12777,6 +12802,7 @@ export class Lowerer { if (webAssembly !== null) return webAssembly; refuseWebAssemblyUse(this, expr.expression); return ( + lowerErrorFunctionCall(this, expr) ?? lowerLifetimeRegionCall(this, expr) ?? lowerInternTableCall(this, expr) ?? lowerThreadSlotsCall(this, expr) ?? diff --git a/packages/compiler/src/frontend/lowering/private-brands.ts b/packages/compiler/src/frontend/lowering/private-brands.ts index 8507cc81a2..c7881bce13 100644 --- a/packages/compiler/src/frontend/lowering/private-brands.ts +++ b/packages/compiler/src/frontend/lowering/private-brands.ts @@ -160,6 +160,60 @@ function brandCall( return { kind: "call", callee: helper, args: [receiver, evaluation], type: resultType, loc }; } +/** The class whose body declares the private name `priv` (the nearest + * enclosing one, as JavaScript scoping resolves it), when it is a + * declaration-level class, or null. */ +export function privateNameClass(lowerer: Lowerer, priv: ts.PrivateIdentifier): ClassInfo | null { + for (let node: ts.Node | undefined = priv.parent; node; node = node.parent) { + if (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) continue; + const declares = node.members.nodes.some((member) => { + const name = (member as { name?: ts.PropertyName }).name; + return name !== undefined && ts.isPrivateIdentifier(name) && name.text === priv.text; + }); + if (!declares) continue; + const symbol = node.name ? lowerer.checker.getSymbolAtLocation(node.name) : undefined; + const info = + lowerer.currentClass?.decl === node + ? lowerer.currentClass + : symbol + ? lowerer.classBySymbol.get(symbol) + : undefined; + if (!info || info.localClass || info.generic || info.genericInstance) return null; + return info; + } + return null; +} + +/** `value.#x` on a checked-dynamic `value` in a declaration-level class + * `info`: the receiver as an instance of `info` when the value is one (an + * instance of the class or a subclass), else Node's TypeError for a missing + * private member (also for undefined and null). */ +export function checkedPrivateValue( + lowerer: Lowerer, + info: ClassInfo, + access: ts.PropertyAccessExpression, + receiver: IrExpr, + loc: SrcLoc, +): IrExpr { + const use: BrandUse = isPrivateWriteTarget(access) ? "write" : "read"; + const className = info.def.name; + const resultType: IrType = { kind: "object", className }; + const helper = `${PREFIX}${use}:${className}:${access.name.text}`; + if (!lowerer.valueHelpers.has(helper)) { + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "value.0", name: "value", type: DYN }], + returnType: resultType, + locals: [{ id: "value.0", name: "value", type: DYN, mutable: false }], + // finalizePrivateBrands fills the body. + body: [], + loc: helperLoc(loc), + }); + lowerer.valueHelpers.set(helper, helper); + } + return { kind: "call", callee: helper, args: [receiver], type: resultType, loc }; +} + /** A plain assignment, a destructuring target, or a for-in/of target writes * the field. Compound assignments and updates read it first, and Node * reports the failed read. */ @@ -203,7 +257,9 @@ export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[]) const name = spec.slice(spec.lastIndexOf(":") + 1); const loc = fn.loc; const value = varRef("value.0", DYN, loc); - const evaluation = varRef("class.0", { kind: "classval", className }, loc); + // A declaration-level class has one brand: the helper takes no class. + const evaluation = + fn.params.length > 1 ? varRef("class.0", { kind: "classval", className }, loc) : null; const ownerType: IrType = { kind: "object", className }; const body: IrStmt[] = []; let matches = 0; @@ -218,21 +274,23 @@ export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[]) const local = `receiver.${matches++}`; fn.locals.push({ id: local, name: "receiver", type: ownerType, mutable: false }); const receiver = varRef(local, ownerType, loc); - const sameEvaluation: IrExpr = { - kind: "bin", - op: "===", - left: { - kind: "fieldGet", - obj: receiver, - className, - field: `%classEnvironment:${className}`, - type: evaluation.type, - loc, - }, - right: evaluation, - type: BOOL, - loc, - }; + const sameEvaluation: IrExpr = evaluation + ? { + kind: "bin", + op: "===", + left: { + kind: "fieldGet", + obj: receiver, + className, + field: `%classEnvironment:${className}`, + type: evaluation.type, + loc, + }, + right: evaluation, + type: BOOL, + loc, + } + : { kind: "boolLit", value: true, type: BOOL, loc }; const match = (typed: IrExpr): IrStmt[] => [ { kind: "varDecl", localId: local, init: typed, loc }, { diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index c779383376..adc11e35a4 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2262,6 +2262,7 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ "date.newLocal", "date.nativeNewFields", "date.nativeNewValue", + "date.argumentsTime", ], note: "zero arguments, one milliseconds/date-string argument, or local-time year/month fields; values are the read-only TimeClip scalar slice", }, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index ca8c521186..b899e52770 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -534,6 +534,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "date.nativeNew", "date.nativeNewFields", "date.nativeNewValue", + "date.argumentsTime", "date.checkedValue", "util.parseArgs", "util.parseEnv", @@ -601,6 +602,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.objCreateWithProperties", "dyn.getPrototype", "dyn.setPrototype", + // Error.captureStackTrace: Node's TypeError for a primitive target, and + // the error string conversion of name and message + "dyn.errorCaptureStackTrace", // a proxy in the prototype chain can throw "dyn.builtinInstanceOf", "dyn.functionInstanceOf", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fa78765791..182d787c86 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -272,6 +272,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.nativeMapIs": { argTypes: [DYN], result: BOOL }, "dyn.nativeUrlIs": { argTypes: [DYN], result: BOOL }, "dyn.nativeDateIs": { argTypes: [DYN], result: BOOL }, + "dyn.nativeDateHandleIs": { argTypes: [DYN], result: BOOL }, /** A builtin constructor (`Object`, `Date`, `RegExp`, `Map`, `Set`, * `Promise`, `WeakMap`, `WeakSet`) read as a JavaScript value: one * interned function per name, the same one dynamic values answer for @@ -291,6 +292,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** `new Date(value)` in JavaScript: a string argument parses with V8's * full date parser; every other value constructs as date.nativeNew. */ "date.nativeNewValue": { argTypes: [DYN], result: DYN }, + "date.argumentsTime": { argTypes: [DYN], result: DATE_T }, + "date.classOperation": { argTypes: [DYN, DYN], result: VOID }, "date.checkedValue": { argTypes: [DYN], result: DATE_T }, "dyn.nativeRegexIs": { argTypes: [DYN], result: BOOL }, /** toString() on a checked-dynamic receiver: runtime kind dispatch @@ -3646,6 +3649,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.arrayPrototype": { argTypes: [], result: DYN }, "dyn.arrayConstructor": { argTypes: [], result: DYN }, "dyn.objectPrototype": { argTypes: [], result: DYN }, + "dyn.errorPrototype": { argTypes: [], result: DYN }, + "dyn.inheritsErrorPrototype": { argTypes: [DYN], result: BOOL }, + "dyn.errorCaptureStackTrace": { argTypes: [DYN], result: DYN }, + "dyn.errorCaptureStackTraceFunction": { argTypes: [], result: DYN }, "dyn.functionApply": { argTypes: [], result: DYN }, "dyn.builtinMethod": { argTypes: [STRING, STRING], result: DYN }, "dyn.classPrototype": { argTypes: [DYN, DYN], result: DYN }, diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 6ff148c55c..21223862a4 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -349,6 +349,7 @@ const DYN_READ_ONLY = new Set([ "date.nativeNew", "date.nativeNewFields", "date.nativeNewValue", + "date.argumentsTime", // The dynamic operators run the ToPrimitive and coercion helpers of // dyn.compare, dyn.numberConstructor and dyn.toStringCoerce, then build a // fresh number, string or bigint. dyn.increment steps dyn.toNumeric's result. diff --git a/packages/compiler/src/ir/tiers.ts b/packages/compiler/src/ir/tiers.ts index ce2fdf0f40..13c4641497 100644 --- a/packages/compiler/src/ir/tiers.ts +++ b/packages/compiler/src/ir/tiers.ts @@ -72,6 +72,7 @@ const DYN_KIND_TESTS: ReadonlySet = new Set([ "dyn.nativeMapIs", "dyn.nativeUrlIs", "dyn.nativeDateIs", + "dyn.nativeDateHandleIs", "dyn.nativeRegexIs", "dyn.dataViewIs", "dyn.typeof", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7040-es5-error-constructor.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7040-es5-error-constructor.mjs.json new file mode 100644 index 0000000000..1a1a6951cc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7040-es5-error-constructor.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7040-es5-error-constructor.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7041-error-called-without-new.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7041-error-called-without-new.mjs.json new file mode 100644 index 0000000000..f40c041168 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7041-error-called-without-new.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7041-error-called-without-new.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7042-string-method-names-on-objects.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7042-string-method-names-on-objects.mjs.json new file mode 100644 index 0000000000..c867cd88ca --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7042-string-method-names-on-objects.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7042-string-method-names-on-objects.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7043-date-set-time.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7043-date-set-time.mjs.json new file mode 100644 index 0000000000..8b369c6331 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7043-date-set-time.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7043-date-set-time.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7044-nested-empty-object-maps.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7044-nested-empty-object-maps.mjs.json new file mode 100644 index 0000000000..ee3592ba6b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7044-nested-empty-object-maps.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7044-nested-empty-object-maps.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7045-instanceof-constructor-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7045-instanceof-constructor-values.mjs.json new file mode 100644 index 0000000000..530aca0ad9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7045-instanceof-constructor-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7045-instanceof-constructor-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7046-parse-float-extra-arguments.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7046-parse-float-extra-arguments.mjs.json new file mode 100644 index 0000000000..0f37ad2a2e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7046-parse-float-extra-arguments.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7046-parse-float-extra-arguments.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7047-dynamic-string-index.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7047-dynamic-string-index.mjs.json new file mode 100644 index 0000000000..7cd38cfb9f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7047-dynamic-string-index.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7047-dynamic-string-index.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7048-unary-number-coercions.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7048-unary-number-coercions.mjs.json new file mode 100644 index 0000000000..10c99f1651 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7048-unary-number-coercions.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7048-unary-number-coercions.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7049-member-assignment-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7049-member-assignment-values.mjs.json new file mode 100644 index 0000000000..5dc0661278 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7049-member-assignment-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7049-member-assignment-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7050-date-subclass.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7050-date-subclass.mjs.json new file mode 100644 index 0000000000..fc73bb4791 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7050-date-subclass.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7050-date-subclass.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7051-date-subclass-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7051-date-subclass-typed.ts.json new file mode 100644 index 0000000000..f37cd97638 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7051-date-subclass-typed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7051-date-subclass-typed.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7052-date-subclass-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7052-date-subclass-dynamic.mjs.json new file mode 100644 index 0000000000..7077aa5bbc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7052-date-subclass-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7052-date-subclass-dynamic.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7053-private-fields-untyped-receivers.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7053-private-fields-untyped-receivers.mjs.json new file mode 100644 index 0000000000..d7d8ba2e1a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7053-private-fields-untyped-receivers.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7053-private-fields-untyped-receivers.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7054-let-bindings-with-null-writes.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7054-let-bindings-with-null-writes.mjs.json new file mode 100644 index 0000000000..25dcd36809 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7054-let-bindings-with-null-writes.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7054-let-bindings-with-null-writes.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 02c4d828f2..5d68b2c6d8 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -662,6 +662,40 @@ bool scr_dyn_native_url_is(const ScrDyn *value); ScrDyn *scr_dyn_native_search_params(ScrSearchParams *value); bool scr_dyn_native_search_params_is(const ScrDyn *value); bool scr_dyn_native_date_is(const ScrDyn *value); +/* Instances of program classes declared `extends Date` (scr_lib.c). */ +void scr_date_class_operation_install(ScrDyn *time, ScrDyn *methods); +/* Set by scr_date_class_operation_install; NULL in a program without such + * a class, where every wrapper below answers false or NULL. */ +typedef struct { + bool (*time)(const ScrDyn *value, double *time); + ScrDyn *(*to_json)(const ScrDyn *value, const ScrDyn *key); + ScrDyn *(*to_string)(const ScrDyn *value); + ScrDyn *(*value_of)(const ScrDyn *value); + ScrStr *(*name)(const ScrDyn *value); + ScrDyn *(*method)(const ScrDyn *value, const char *key, size_t key_len); +} ScrDateClassHooks; +extern const ScrDateClassHooks *scr_date_class_hooks; +static inline bool scr_dyn_date_class_time(const ScrDyn *value, double *time) { + return scr_date_class_hooks && scr_date_class_hooks->time(value, time); +} +static inline ScrDyn *scr_dyn_date_class_to_json(const ScrDyn *value, const ScrDyn *key) { + return scr_date_class_hooks->to_json(value, key); +} +static inline ScrDyn *scr_dyn_date_class_to_string(const ScrDyn *value) { + return scr_date_class_hooks->to_string(value); +} +static inline ScrDyn *scr_dyn_date_class_value_of(const ScrDyn *value) { + return scr_date_class_hooks->value_of(value); +} +static inline ScrStr *scr_dyn_date_class_name(const ScrDyn *value) { + return scr_date_class_hooks ? scr_date_class_hooks->name(value) : NULL; +} +static inline ScrDyn *scr_dyn_date_class_method(const ScrDyn *value, const char *key, size_t key_len) { + return scr_date_class_hooks ? scr_date_class_hooks->method(value, key, key_len) : NULL; +} +bool scr_dyn_date_instance_is(const ScrDyn *value); +ScrDyn *scr_dyn_date_class_member(const ScrDyn *d, const char *key, size_t key_len); +double scr_dyn_date_arguments_time(const ScrDyn *arguments); ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments); ScrDyn *scr_dyn_native_date_new_fields(const ScrDyn *arguments); ScrDyn *scr_dyn_native_date_new_value(const ScrDyn *arguments); @@ -1048,6 +1082,10 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc); * prototype chain has no toString (Node's answer). */ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what); ScrDyn *scr_dyn_object_prototype(void); +ScrDyn *scr_dyn_error_prototype(void); +bool scr_dyn_inherits_error_prototype(const ScrDyn *value); +ScrDyn *scr_dyn_error_capture_stack_trace(ScrDyn *target); +ScrDyn *scr_dyn_error_capture_stack_trace_function(void); ScrDyn *scr_dyn_function_apply(void); ScrDyn *scr_dyn_reflect_apply(ScrDyn *target, ScrDyn *receiver, ScrDyn *arguments_list); ScrDyn *scr_dyn_apply_array_like(const ScrDyn *target, const ScrDyn *receiver, const ScrDyn *arguments, const char *what); diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 2a010f2131..ec49dd3831 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -845,6 +845,16 @@ static ScrDyn *scr_dyn_invoke_impl( if (scr_event_ref_hooks && scr_event_ref_hooks->invoke(recv, method, args, argc, what, &fast)) return fast; + /* An inherited Date method of an instance of a class declared `extends + * Date`. */ + ScrDyn *date_method = scr_dyn_date_class_member(recv, method, strlen(method)); + if (date_method) { + scr_dyn_this_push_dyn(recv); + ScrDyn *result = scr_dyn_call(date_method, args, argc, what); + scr_dyn_this_pop(); + scr_dyn_release(date_method); + return result; + } ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv); if (scr_exc_pending()) { scr_dyn_release(materialized); diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 223517e925..a4d124ac3c 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -1652,7 +1652,40 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { * view of its fields does not carry. */ const ScrStr *name = scr_dyn_typed_ref_class_name(d); ScrStr *out; - if (name && materialized->kind == SCR_DYN_OBJ && + double time; + ScrStr *date_class = materialized->kind == SCR_DYN_OBJ && scr_dyn_date_class_time(d, &time) + ? scr_dyn_date_class_name(d) : NULL; + if (date_class) { + /* An instance of a class declared `extends Date`: the class name and + * the time as Date.prototype.toISOString formats it, then its own + * enumerable properties when it has any. */ + ScrStr *iso = scr_insp_date(time); + InspBuf head = {0}; + ib_bytes(&head, date_class->data, date_class->len); + scr_str_release(date_class); + ib_char(&head, ' '); + ib_bytes(&head, iso->data, iso->len); + scr_str_release(iso); + ScrStr *prefix = ib_take(&head); + bool fields = false; + for (size_t i = 0; i < materialized->v.obj.len && !fields; i++) + fields = materialized->v.obj.entries[i].enumerable; + if (!fields || recurse > depth) { + scr_dyn_release(materialized); + return prefix; + } + char *cls = malloc(prefix->len + 1); + if (!cls) scr_trap("scriptc: out of memory\n"); + memcpy(cls, prefix->data, prefix->len); + cls[prefix->len] = '\0'; + scr_str_release(prefix); + size_t nseen = g_nseen; + g_pending_seen = instance; + out = scr_insp_obj(materialized, recurse, depth, cls); + g_pending_seen = NULL; + if (g_nseen > nseen) g_nseen = nseen; + free(cls); + } else if (name && materialized->kind == SCR_DYN_OBJ && scr_dyn_obj_get(materialized, "%error", 6) == NULL) { char *cls = malloc(name->len + 1); if (!cls) scr_trap("scriptc: out of memory\n"); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index b8f16630b5..549ca91656 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -37,6 +37,19 @@ static SCR_TL ScrDyn *scr_builtin_object_prototype; +const ScrDateClassHooks *scr_date_class_hooks; + +/* The checked-dynamic Error.prototype (scr_dyn_error_prototype) is not the + * prototype of scriptc's Error instances, so a change to it would not reach + * them: every mutation throws. */ +static SCR_TL ScrDyn *scr_builtin_error_prototype; +static bool scr_dyn_error_prototype_fence(const ScrDyn *value) { + if (!value || value != scr_builtin_error_prototype) return false; + static const char message[] = "Error.prototype mutation is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return true; +} + /* Constructor capsules expose identity and name/length, but do not model * per-class static property tables yet. Never report a fabricated empty view. */ static bool scr_dyn_class_reflection_fence(const ScrDyn *value) { @@ -1816,6 +1829,149 @@ ScrDyn *scr_dyn_array_prototype_base(void) { return scr_dyn_retain(scr_builtin_array_prototype); } +/* Error.prototype as a checked-dynamic object, for JavaScript constructor + * functions that inherit from it (`F.prototype = + * Object.create(Error.prototype)`): non-enumerable `constructor`, `name` + * ("Error"), `message` ("") and `toString`, on Object.prototype. The Error + * constructor has no checked-dynamic value, so `constructor` is a function + * named Error that throws when called. */ +static const ScrDyn *scr_dyn_proto_next(const ScrDyn *object); +static void scr_builtin_error_prototype_cleanup(void) { + ScrDyn *prototype = scr_builtin_error_prototype; + scr_builtin_error_prototype = NULL; + scr_dyn_release(prototype); +} +static ScrDyn *scr_error_prototype_to_string(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + ScrDyn *self = scr_dyn_this_get(); + ScrDyn *result = NULL; + if (self->kind == SCR_DYN_OBJ || self->kind == SCR_DYN_ARR || self->kind == SCR_DYN_FUNC) { + ScrStr *text = scr_dyn_error_string(self); + if (text) { + result = scr_dyn_new_str(text); + scr_str_release(text); + } + } else { + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "Method Error.prototype.toString called on incompatible receiver "); + ScrStr *shown = self->kind == SCR_DYN_SYMBOL ? self->v.symbol.render(self->v.symbol.value) + : scr_dyn_string_coerce_js(self); + if (shown) { + scr_jb_put_str(&message, shown); + scr_str_release(shown); + } + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message)); + } + scr_dyn_release(self); + return result; +} +static ScrDyn *scr_error_prototype_constructor(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + static const char message[] = "calling Error.prototype.constructor is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; +} +static void scr_error_prototype_hide(ScrDyn *object, const char *key, size_t key_len) { + ScrDynEntry *entry = scr_dyn_find_entry(object, key, key_len); + entry->enumerable = false; +} +ScrDyn *scr_dyn_error_prototype(void) { + if (!scr_builtin_error_prototype) { + ScrDyn *prototype = scr_dyn_alloc(SCR_DYN_OBJ); + scr_dyn_obj_set(prototype, "constructor", 11, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_prototype_constructor, 1, "native:Error", "Error")); + scr_error_prototype_hide(prototype, "constructor", 11); + ScrStr *name = scr_str_new("Error", 5); + scr_dyn_obj_set(prototype, "name", 4, scr_dyn_new_str(name)); + scr_str_release(name); + scr_error_prototype_hide(prototype, "name", 4); + ScrStr *empty = scr_str_new("", 0); + scr_dyn_obj_set(prototype, "message", 7, scr_dyn_new_str(empty)); + scr_str_release(empty); + scr_error_prototype_hide(prototype, "message", 7); + scr_dyn_obj_set(prototype, "toString", 8, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_prototype_to_string, 0, "native:Error", "toString")); + scr_error_prototype_hide(prototype, "toString", 8); + scr_builtin_error_prototype = prototype; + scr_atexit(scr_builtin_error_prototype_cleanup); + } + return scr_dyn_retain(scr_builtin_error_prototype); +} + +/* Error.captureStackTrace(target): defines a non-enumerable own `stack` + * on target. Node's value is the Error.prototype.toString text of target + * (read without calling an overriding toString) followed by stack frames; + * here, as for scriptc's own Error instances, it is the text alone. + * Answers undefined, or NULL with the exception pending. */ +ScrDyn *scr_dyn_error_capture_stack_trace(ScrDyn *target) { + /* An instance of a class that extends Error derives its stack from its + * name and message when the stack is read. */ + if (target->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(target); + bool error = view && view->kind == SCR_DYN_OBJ && scr_dyn_obj_get(view, "%error", 6) != NULL; + scr_dyn_release(view); + if (scr_exc_pending()) return NULL; + if (error) return scr_dyn_retain(scr_dyn_undefined()); + } + if (target->kind != SCR_DYN_OBJ) { + if (target->kind == SCR_DYN_ARR || target->kind == SCR_DYN_FUNC || target->kind == SCR_DYN_TYPED_REF || + target->kind == SCR_DYN_HANDLE || target->kind == SCR_DYN_PROXY) { + static const char message[] = "Error.captureStackTrace on this object is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + } else { + static const char message[] = "invalid_argument"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } + return NULL; + } + ScrDynEntry *existing = scr_dyn_find_entry(target, "stack", 5); + if (existing ? existing->accessor || !existing->configurable : target->non_extensible) { + static const char message[] = "Cannot redefine property: stack"; + static const char extensible[] = "Cannot define property stack, object is not extensible"; + if (existing) scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + else scr_throw_error_msg(SCR_ERR_TYPE, extensible, sizeof extensible - 1); + return NULL; + } + ScrStr *text = scr_dyn_error_string(target); + if (!text) return NULL; + scr_dyn_obj_set_field(target, "stack", 5, scr_dyn_new_str(text)); + scr_str_release(text); + ScrDynEntry *entry = scr_dyn_find_entry(target, "stack", 5); + entry->enumerable = false; + entry->writable = entry->configurable = true; + return scr_dyn_retain(scr_dyn_undefined()); +} +static ScrDyn *scr_error_capture_stack_trace_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + return scr_dyn_error_capture_stack_trace(argc > 0 ? args[0] : scr_dyn_undefined()); +} +/* Error.captureStackTrace read as a value: one function per thread. */ +static SCR_TL ScrDyn *scr_error_capture_stack_trace_fn; +static void scr_error_capture_stack_trace_fn_cleanup(void) { + ScrDyn *function = scr_error_capture_stack_trace_fn; + scr_error_capture_stack_trace_fn = NULL; + scr_dyn_release(function); +} +ScrDyn *scr_dyn_error_capture_stack_trace_function(void) { + if (!scr_error_capture_stack_trace_fn) { + scr_error_capture_stack_trace_fn = scr_dyn_new_func(scr_closure_new(NULL, 0), + scr_error_capture_stack_trace_call, 2, "native:Error", "captureStackTrace"); + scr_atexit(scr_error_capture_stack_trace_fn_cleanup); + } + return scr_dyn_retain(scr_error_capture_stack_trace_fn); +} + +/* `value instanceof Error` for a value that is not an Error instance: true + * when Error.prototype is on its prototype chain. */ +bool scr_dyn_inherits_error_prototype(const ScrDyn *value) { + if (!scr_builtin_error_prototype || !value || value->kind != SCR_DYN_OBJ) return false; + for (const ScrDyn *current = scr_dyn_proto_next(value); current; current = scr_dyn_proto_next(current)) { + if (current == scr_builtin_error_prototype) return true; + } + return false; +} + ScrDyn *scr_dyn_new_arr(void) { return scr_dyn_alloc(SCR_DYN_ARR); } ScrDyn *scr_dyn_new_obj(void) { return scr_dyn_alloc(SCR_DYN_OBJ); } ScrDyn *scr_dyn_new_obj_with_identity( @@ -1847,7 +2003,6 @@ ScrDyn *scr_dyn_new_obj_null_proto(void) { } static const ScrDyn *scr_dyn_chain_link(const ScrDyn *link); -static const ScrDyn *scr_dyn_proto_next(const ScrDyn *object); /* A typed capsule can be a prototype when its checked view is an object. */ static bool scr_dyn_typed_ref_prototype(const ScrDyn *value) { @@ -1942,7 +2097,7 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { object->kind == SCR_DYN_BIGINT || object->kind == SCR_DYN_SYMBOL) { return scr_dyn_prototype_valid(prototype) ? scr_dyn_retain(object) : NULL; } - if (scr_dyn_class_reflection_fence(object) || scr_dyn_class_reflection_fence(prototype)) return NULL; + if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object) || scr_dyn_class_reflection_fence(prototype)) return NULL; if (object->kind == SCR_DYN_TYPED_REF) { ScrDyn *view = scr_dyn_typed_ref_materialize(object); if (scr_exc_pending()) { scr_dyn_release(view); return NULL; } @@ -2058,7 +2213,7 @@ static ScrDyn *scr_dyn_fn_existing_table(const ScrDyn *function) { } ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) { - if (scr_dyn_class_reflection_fence(object)) return NULL; + if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.preventExtensions"); return NULL; } if (scr_dyn_class_integrity_fence(object, "Object.preventExtensions")) return NULL; ScrDyn *table = scr_dyn_fn_integrity_table(object); @@ -2089,7 +2244,7 @@ bool scr_dyn_is_extensible(ScrDyn *object) { } ScrDyn *scr_dyn_seal(ScrDyn *object) { - if (scr_dyn_class_reflection_fence(object)) return NULL; + if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.seal"); return NULL; } if (scr_dyn_class_integrity_fence(object, "Object.seal")) return NULL; if (object->symbol_properties) scr_dyn_release(scr_dyn_seal(object->symbol_properties)); @@ -3366,6 +3521,7 @@ const ScrStr *scr_dyn_typed_ref_class_name(const ScrDyn *d) { return ops ? ops->class_name : NULL; } + static bool scr_dyn_name_in_strs(const ScrStr *key, ScrStr *const *names, size_t count) { for (size_t i = 0; i < count; i++) { if (names[i] == key || scr_str_eq(names[i], (ScrStr *)key)) return true; @@ -3457,6 +3613,18 @@ ScrDyn *scr_dyn_index_get(const ScrDyn *d, double index) { } ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) { + /* An instance of a class declared `extends Date` converts through its + * toString, declared or inherited. With an exception pending the view is + * the empty string. */ + double time; + if (scr_dyn_date_class_time(d, &time)) { + ScrDyn *text = scr_dyn_date_class_to_string(d); + if (text) return text; + ScrStr *empty = scr_str_new("", 0); + ScrDyn *out = scr_dyn_new_str(empty); + scr_str_release(empty); + return out; + } ScrDyn *view = scr_dyn_typed_ref_materialize(d); const char *tag = scr_event_ref_hooks ? scr_event_ref_hooks->string_tag(d) : NULL; if (!tag || view->kind != SCR_DYN_OBJ || scr_dyn_property_owner(view, "toString", 8)) return view; @@ -3467,12 +3635,28 @@ ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) { return out; } +/* An inherited Date method of an instance of a class declared `extends + * Date`, when the instance's own members do not define `key`: +1, or + * NULL. */ +ScrDyn *scr_dyn_date_class_member(const ScrDyn *d, const char *key, size_t key_len) { + ScrDyn *method = scr_dyn_date_class_method(d, key, key_len); + if (!method) return NULL; + ScrDyn *view = scr_dyn_typed_ref_materialize(d); + bool declared = view && !scr_exc_pending() && scr_dyn_property_owner(view, key, key_len); + scr_dyn_release(view); + if (!declared) return method; + scr_dyn_release(method); + 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) { ScrDyn *inherited = scr_event_ref_hooks->get(d, key); if (inherited) return inherited; } + ScrDyn *date_method = scr_dyn_date_class_member(d, key->data, key->len); + if (date_method) return date_method; 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); size_t index; @@ -3708,6 +3892,29 @@ ScrDyn *scr_array_buffer_constructor(void) { return scr_dyn_retain(scr_array_buffer_ctor); } +/* `value instanceof C` for C a global constructor held as a value + * (`{ instanceOf: Date }`, scr_dyn_global_constructor): 1 or 0 by the + * value's native kind, or -1 when callee is not one of them, or for Object + * and for class instances, which these kinds cannot answer. A typed record + * or array is none of these kinds. */ +static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrDyn *callee) { + if (!callee->v.fn.sig || !callee->v.fn.name || strcmp(callee->v.fn.sig, "native:constructor") != 0 || + strcmp(callee->v.fn.name, "Object") == 0) + return -1; + if (value->kind == SCR_DYN_TYPED_REF) + return strncmp(value->v.typed_ref.type_key, "record:", 7) == 0 || scr_dyn_ref_is_array(value) ? 0 : -1; + static const struct { const char *name; int tag; } kinds[] = { + {"Date", SCR_DYNH_DATE}, {"RegExp", SCR_DYNH_REGEXP}, {"Map", SCR_DYNH_MAP}, {"Set", SCR_DYNH_SET}, + {"WeakMap", SCR_DYNH_WEAK_MAP}, {"WeakSet", SCR_DYNH_WEAK_SET}, + }; + if (strcmp(callee->v.fn.name, "Promise") == 0) return value->kind == SCR_DYN_PROMISE; + for (size_t i = 0; i < sizeof kinds / sizeof kinds[0]; i++) { + if (strcmp(callee->v.fn.name, kinds[i].name) == 0) + return value->kind == SCR_DYN_HANDLE && (int)value->v.handle.tag == kinds[i].tag; + } + return -1; +} + bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { if (!callee || callee->kind != SCR_DYN_FUNC) { static const char message[] = "Right-hand side of 'instanceof' is not callable"; @@ -3720,6 +3927,38 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { if (scr_bytes_ctors[i] && callee->v.fn.clo == scr_bytes_ctors[i]->v.fn.clo) return scr_dyn_typed_array_is(value, (int)i); } + int global = scr_dyn_global_constructor_instanceof(value, callee); + if (global >= 0) return global; + /* An ordinary constructor function (an untyped JavaScript function whose + * prototype is an object): OrdinaryHasInstance walks the value's + * prototype chain for callee.prototype. */ + { + ScrStr *name = scr_str_new("prototype", 9); + ScrDyn *key = scr_dyn_new_str(name); + scr_str_release(name); + ScrDyn *prototype = scr_dyn_reflect_get((ScrDyn *)callee, key, (ScrDyn *)callee); + scr_dyn_release(key); + if (!prototype) return false; /* the read threw: pending */ + if (prototype->kind == SCR_DYN_OBJ) { + bool found = false; + if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_FUNC) { + ScrDyn *current = scr_dyn_get_prototype((ScrDyn *)value); + while (current && current->kind != SCR_DYN_NULL) { + if (current == prototype) { + found = true; + break; + } + ScrDyn *next = scr_dyn_get_prototype(current); + scr_dyn_release(current); + current = next; + } + if (current) scr_dyn_release(current); + } + scr_dyn_release(prototype); + return found; + } + scr_dyn_release(prototype); + } 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; @@ -6191,6 +6430,7 @@ void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key) { } void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored) { + if (scr_dyn_error_prototype_fence(value)) return; ScrDyn *receiver = scr_dyn_symbol_receiver(value); if (!receiver) { if (!scr_exc_pending()) { @@ -6341,6 +6581,7 @@ static bool scr_dyn_fn_property_fence(const ScrStr *key) { } ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) { + if (scr_dyn_error_prototype_fence(target)) return NULL; if (target->kind == SCR_DYN_TYPED_REF) { ScrDyn *view = scr_dyn_typed_ref_materialize(target); ScrDyn *defined = scr_exc_pending() ? NULL : scr_dyn_define_property(view, key, descriptor); @@ -7726,6 +7967,9 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { } } +static ScrStr *scr_dyn_to_string_method_args(const ScrDyn *d, const ScrStr *enc, const ScrStr *what, + ScrDyn *const *args, size_t argc); + /* The METHOD-CALL spelling `d.toString(enc?)` — scr_dyn_to_string with * the one receiver whose prototype LACKS the method carved out: a * null-prototype dictionary (Object.create(null)) has no toString at @@ -7755,7 +7999,10 @@ ScrStr *scr_dyn_to_string_argument(const ScrDyn *d, const ScrDyn *argument, cons scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); return scr_str_new("", 0); } - return scr_dyn_to_string_method(d, NULL, what); + /* A toString the program defines receives the argument + * (`error.toString(compact)`); undefined passes none. */ + ScrDyn *args[1] = {(ScrDyn *)argument}; + return scr_dyn_to_string_method_args(d, NULL, what, args, argument->kind == SCR_DYN_UNDEF ? 0 : 1); } /* The METHOD-CALL spelling `d.valueOf()`: a primitive answers itself (the @@ -7995,7 +8242,12 @@ bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out) { return false; } if (d->kind == SCR_DYN_TYPED_REF) { - ScrDyn *materialized = scr_dyn_typed_ref_materialize(d); + /* An instance of a class declared `extends Date` converts through its + * valueOf, declared or inherited. */ + double time; + ScrDyn *materialized = scr_dyn_date_class_time(d, &time) ? scr_dyn_date_class_value_of(d) + : scr_dyn_typed_ref_materialize(d); + if (!materialized) return false; bool ok = scr_dyn_number_coerce_js(materialized, out); scr_dyn_release(materialized); return ok; @@ -8098,9 +8350,18 @@ double scr_dyn_number_coerce(const ScrDyn *d) { static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d); /* ToPrimitive with the number hint, which differs from the default hint - * only for a Date: its time value. */ + * only for a Date: its time value, or the valueOf of an instance of a class + * declared `extends Date`. */ static ScrDyn *scr_dyn_number_primitive(const ScrDyn *d) { - return scr_dyn_is_date(d) ? scr_dyn_date_value_of(d) : scr_dyn_add_primitive(d); + if (scr_dyn_is_date(d)) return scr_dyn_date_value_of(d); + double time; + if (d->kind == SCR_DYN_TYPED_REF && scr_dyn_date_class_time(d, &time)) { + ScrDyn *value = scr_dyn_date_class_value_of(d); + ScrDyn *out = value ? scr_dyn_add_primitive(value) : NULL; + scr_dyn_release(value); + return out; + } + return scr_dyn_add_primitive(d); } double scr_dyn_number_constructor(const ScrDyn *d) { @@ -8161,6 +8422,15 @@ static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d) { return NULL; } if (d->kind == SCR_DYN_TYPED_REF) { + /* A Date's default hint is string: an instance of a class declared + * `extends Date` adds through its toString. */ + double time; + if (scr_dyn_date_class_time(d, &time)) { + ScrDyn *text = scr_dyn_date_class_to_string(d); + ScrDyn *out = text ? scr_dyn_add_primitive(text) : NULL; + scr_dyn_release(text); + return out; + } ScrDyn *view = scr_dyn_typed_ref_materialize(d); ScrDyn *out = scr_exc_pending() ? NULL : scr_dyn_add_primitive(view); scr_dyn_release(view); @@ -8580,7 +8850,7 @@ void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn } void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { - if (scr_dyn_class_reflection_fence(recv)) return; + if (scr_dyn_class_reflection_fence(recv) || scr_dyn_error_prototype_fence(recv)) return; if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_set(recv, key, value); return; } if (recv->kind == SCR_DYN_TYPED_REF) { if (scr_typed_ref_array_set(recv, key, value)) return; @@ -10408,7 +10678,7 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) { * arrays, typed references and handles keep their explicit boundary; * deleting from a materialized snapshot would lose the mutation. */ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) { - if (scr_dyn_class_reflection_fence(recv)) return; + if (scr_dyn_class_reflection_fence(recv) || scr_dyn_error_prototype_fence(recv)) return; if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_delete(recv, key); return; } if (recv->kind == SCR_DYN_TYPED_REF && recv->v.typed_ref.commit && (!strncmp(recv->v.typed_ref.type_key, "record:", 7) || @@ -10634,6 +10904,16 @@ static bool scr_json_live_container(const ScrDyn *value, bool inspect) { * method, called with the key. Returns the value to serialize (+1), or NULL * with an exception pending. */ static ScrDyn *scr_json_to_json(ScrDyn *value, const ScrStr *key) { + /* An instance of a class declared `extends Date` converts through its + * toJSON, declared or inherited. */ + double time; + if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_date_class_time(value, &time)) { + ScrDyn *name = scr_dyn_new_str((ScrStr *)key); + ScrDyn *converted = scr_dyn_date_class_to_json(value, name); + scr_dyn_release(name); + scr_dyn_release(value); + return converted; + } /* Inspect a typed reference without replacing its live identity. Later * members must read the source again after a callback mutates it. A live * array, or a live record without a `toJSON` field, needs no conversion. */ @@ -12826,7 +13106,7 @@ static void scr_dyn_assign_from(ScrDyn *target, const ScrDyn *src, bool define) * nothing; index-keyed sources (arrays/strings/bytes) copy their index * keys like Node; the remaining kinds have no own enumerable keys. */ ScrDyn *scr_dyn_assign(ScrDyn *target, const ScrDyn *src) { - if (scr_dyn_class_reflection_fence(target) || scr_dyn_class_reflection_fence(src)) return NULL; + if (scr_dyn_class_reflection_fence(target) || scr_dyn_error_prototype_fence(target) || scr_dyn_class_reflection_fence(src)) return NULL; if (target->kind == SCR_DYN_UNDEF || target->kind == SCR_DYN_NULL) { const char *m = "Cannot convert undefined or null to object"; scr_throw_error_msg(SCR_ERR_TYPE, m, strlen(m)); @@ -14620,7 +14900,10 @@ ScrMap *scr_dyn_native_collection_check(const ScrDyn *value, int map, const char return scr_map_retain(value->v.handle.ptr); } -ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what) { +/* scr_dyn_to_string_method, passing `args` to a toString the receiver's + * chain defines. */ +static ScrStr *scr_dyn_to_string_method_args(const ScrDyn *d, const ScrStr *enc, const ScrStr *what, + ScrDyn *const *args, size_t argc) { /* Object.create receivers must run their inherited formatter before the * default object tag; Effect shard keys use it to produce distinct hashes. */ const bool object_method = d->kind == SCR_DYN_OBJ && scr_dyn_property_owner(d, "toString", 8); @@ -14629,7 +14912,7 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt if (scr_exc_pending()) { scr_dyn_release(member); return NULL; } if (member && (object_method || member->kind != SCR_DYN_UNDEF)) { scr_dyn_this_push_dyn(d); - ScrDyn *value = scr_dyn_call(member, NULL, 0, what->data); + ScrDyn *value = scr_dyn_call(member, args, argc, what->data); scr_dyn_this_pop(); scr_dyn_release(member); if (!value) return NULL; @@ -14651,8 +14934,12 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt return scr_dyn_to_string(d, enc); } +ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what) { + return scr_dyn_to_string_method_args(d, enc, what, NULL, 0); +} + ScrDyn *scr_dyn_freeze(ScrDyn *value) { - if (scr_dyn_class_reflection_fence(value)) return NULL; + if (scr_dyn_class_reflection_fence(value) || scr_dyn_error_prototype_fence(value)) return NULL; if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL; ScrDyn *table = scr_dyn_fn_integrity_table(value); if (!table) return scr_dyn_freeze_data(value); @@ -14663,7 +14950,7 @@ ScrDyn *scr_dyn_freeze(ScrDyn *value) { } ScrDyn *scr_dyn_freeze_data(ScrDyn *value) { - if (scr_dyn_class_reflection_fence(value)) return NULL; + if (scr_dyn_class_reflection_fence(value) || scr_dyn_error_prototype_fence(value)) return NULL; if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL; if (value->symbol_properties) scr_dyn_release(scr_dyn_freeze_data(value->symbol_properties)); if (value->kind == SCR_DYN_ARR) { diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 3441e61425..d856e3000e 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -12997,9 +12997,116 @@ bool scr_dyn_native_date_is(const ScrDyn *value) { return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_DATE; } +/* Instances of program classes declared `extends Date` keep their time + * value in a compiled field. In checked-dynamic code such an instance is a + * typed reference, and the runtime reaches its time value and methods + * through the functions %main installs (builtins/date-classes.ts), called + * as (op, value, argument): for op 0 the time function answers the time + * value, or undefined for any other value, and for op 1 sets it; for ops 2 + * to 5 the method function calls the instance's toISOString, + * toJSON(argument), toString and valueOf. */ +enum { + SCR_DATE_CLASS_TIME, + SCR_DATE_CLASS_SET_TIME, + SCR_DATE_CLASS_TO_ISO_STRING, + SCR_DATE_CLASS_TO_JSON, + SCR_DATE_CLASS_TO_STRING, + SCR_DATE_CLASS_VALUE_OF, + SCR_DATE_CLASS_NAME, +}; +static SCR_TL ScrDyn *scr_date_class_operation; +static SCR_TL ScrDyn *scr_date_class_methods; + +static void scr_date_class_operation_cleanup(void) { + ScrDyn *operation = scr_date_class_operation; + ScrDyn *methods = scr_date_class_methods; + scr_date_class_operation = scr_date_class_methods = NULL; + scr_dyn_release(operation); + scr_dyn_release(methods); +} + +static bool scr_date_class_time_of(const ScrDyn *value, double *time); +static ScrDyn *scr_date_class_to_json(const ScrDyn *value, const ScrDyn *key); +static ScrDyn *scr_date_class_to_string(const ScrDyn *value); +static ScrDyn *scr_date_class_value_of(const ScrDyn *value); +static ScrStr *scr_date_class_name(const ScrDyn *value); +static ScrDyn *scr_date_class_method(const ScrDyn *value, const char *key, size_t key_len); + +/* The installation also publishes these functions to the rest of the + * runtime (scr_date_class_hooks), so only a program that declares a class + * extending Date links them. */ +static const ScrDateClassHooks scr_date_class_hook_table = { + scr_date_class_time_of, scr_date_class_to_json, scr_date_class_to_string, + scr_date_class_value_of, scr_date_class_name, scr_date_class_method, +}; + +void scr_date_class_operation_install(ScrDyn *time, ScrDyn *methods) { + scr_date_class_hooks = &scr_date_class_hook_table; + if (!scr_date_class_operation) scr_atexit(scr_date_class_operation_cleanup); + scr_dyn_release(scr_date_class_operation); + scr_dyn_release(scr_date_class_methods); + scr_date_class_operation = scr_dyn_retain(time); + scr_date_class_methods = scr_dyn_retain(methods); +} + +/* The installed operation `op` on `value`: +1, or NULL with an exception + * pending. */ +static ScrDyn *scr_date_class_call(int op, const ScrDyn *value, const ScrDyn *argument) { + ScrDyn *code = scr_dyn_new_num(op); + ScrDyn *args[] = { code, (ScrDyn *)value, (ScrDyn *)(argument ? argument : scr_dyn_undefined()) }; + bool time = op < SCR_DATE_CLASS_TO_ISO_STRING || op == SCR_DATE_CLASS_NAME; + ScrDyn *result = scr_dyn_call(time ? scr_date_class_operation : scr_date_class_methods, args, 3, "Date"); + scr_dyn_release(code); + return result; +} + +/* True, with the time value in *time, when `value` holds an instance of a + * class declared `extends Date`. */ +static bool scr_date_class_time_of(const ScrDyn *value, double *time) { + if (!scr_date_class_operation || !value || value->kind != SCR_DYN_TYPED_REF) return false; + ScrDyn *result = scr_date_class_call(SCR_DATE_CLASS_TIME, value, NULL); + bool found = result && result->kind == SCR_DYN_NUM; + if (found) *time = result->v.num; + scr_dyn_release(result); + return found; +} + +/* `value instanceof Date`: a Date, or an instance of a class declared + * `extends Date`. */ +bool scr_dyn_date_instance_is(const ScrDyn *value) { + double time; + return scr_dyn_native_date_is(value) || scr_dyn_date_class_time(value, &time); +} + +/* toJSON(key), toString() or valueOf() of an instance of a class declared + * `extends Date` (scr_dyn_date_class_time answered true): the class's + * declaration or the Date method. +1, or NULL with an exception pending. */ +static ScrDyn *scr_date_class_to_json(const ScrDyn *value, const ScrDyn *key) { + return scr_date_class_call(SCR_DATE_CLASS_TO_JSON, value, key); +} +static ScrDyn *scr_date_class_to_string(const ScrDyn *value) { + return scr_date_class_call(SCR_DATE_CLASS_TO_STRING, value, NULL); +} +static ScrDyn *scr_date_class_value_of(const ScrDyn *value) { + return scr_date_class_call(SCR_DATE_CLASS_VALUE_OF, value, NULL); +} + +/* The name of the class of an instance of a class declared `extends + * Date`: +1, or NULL for any other value. */ +static ScrStr *scr_date_class_name(const ScrDyn *value) { + if (!scr_date_class_operation || !value || value->kind != SCR_DYN_TYPED_REF) return NULL; + ScrDyn *result = scr_date_class_call(SCR_DATE_CLASS_NAME, value, NULL); + ScrStr *name = result && result->kind == SCR_DYN_STR ? scr_str_retain(result->v.str) : NULL; + scr_dyn_release(result); + return name; +} + double scr_dyn_native_date_value(const ScrDyn *value) { - if (!scr_dyn_native_date_is(value)) { scr_dyn_check_fail(NULL, "Date", value); return NAN; } - return ((ScrNativeDate *)value->v.handle.ptr)->milliseconds; + if (scr_dyn_native_date_is(value)) return ((ScrNativeDate *)value->v.handle.ptr)->milliseconds; + double time; + if (scr_dyn_date_class_time(value, &time)) return time; + scr_dyn_check_fail(NULL, "Date", value); + return NAN; } /* Method names without another use in this file, in named arrays (see @@ -13009,13 +13116,14 @@ static const char scr_date_to_time_string_name[] = "toTimeString"; static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { - (void)self; (void)args; (void)argc; (void)what; + (void)self; (void)what; double ms = ((ScrNativeDate *)ptr)->milliseconds; if (!strcmp(method, "getTime") || !strcmp(method, "valueOf")) return scr_dyn_new_num(ms); if (!strcmp(method, "toString") || !strcmp(method, scr_date_to_date_string_name) || - !strcmp(method, scr_date_to_time_string_name)) { + !strcmp(method, scr_date_to_time_string_name) || !strcmp(method, "toUTCString")) { ScrStr *text = !strcmp(method, "toString") ? scr_date_to_string(ms) : !strcmp(method, scr_date_to_date_string_name) ? scr_date_to_date_string(ms) + : !strcmp(method, "toUTCString") ? scr_date_to_utc_string(ms) : scr_date_to_time_string(ms); ScrDyn *result = scr_dyn_new_str(text); scr_str_release(text); @@ -13029,6 +13137,12 @@ static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *metho scr_str_release(text); return result; } + if (!strcmp(method, "setTime")) { + double time = NAN; + if (argc > 0 && !scr_dyn_number_coerce_js(args[0], &time)) return NULL; + ((ScrNativeDate *)ptr)->milliseconds = scr_date_new_ms(time); + return scr_dyn_new_num(((ScrNativeDate *)ptr)->milliseconds); + } #define DATE_GET(name, fn) if (!strcmp(method, name)) return scr_dyn_new_num(fn(ms)) DATE_GET("getFullYear", scr_date_get_full_year_local); DATE_GET("getUTCFullYear", scr_date_get_full_year_utc); @@ -13059,13 +13173,25 @@ ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments) { else { ScrDyn *value = arguments->v.arr.items[0]; if (scr_dyn_native_date_is(value)) ms = scr_dyn_native_date_value(value); - else if (value->kind == SCR_DYN_STR) ms = scr_date_parse_get_time(value->v.str); + else if (scr_dyn_date_class_time(value, &ms)) { + } else if (value->kind == SCR_DYN_STR) ms = scr_date_parse_get_time(value->v.str); else if (!scr_dyn_number_coerce_js(value, &ms)) return NULL; ms = scr_date_new_ms(ms); } return scr_dyn_native_date_from_ms(ms); } +/* The time value `new Date(...arguments)` constructs (super(...) in a + * class declared `extends Date`), or NaN with an exception pending. */ +double scr_dyn_date_arguments_time(const ScrDyn *arguments) { + ScrDyn *date = arguments->v.arr.len > 1 ? scr_dyn_native_date_new_fields(arguments) + : scr_dyn_native_date_new_value(arguments); + if (!date) return NAN; + double time = ((ScrNativeDate *)date->v.handle.ptr)->milliseconds; + scr_dyn_release(date); + return time; +} + /* new Date(value) in JavaScript: a string parses with the full parser; * every other value takes scr_dyn_native_date_new. */ ScrDyn *scr_dyn_native_date_new_value(const ScrDyn *arguments) { @@ -13092,11 +13218,11 @@ ScrDyn *scr_dyn_native_date_new_fields(const ScrDyn *arguments) { /* The Date.prototype methods scr_native_date_invoke answers. */ static const char *const scr_native_date_methods[] = { "getTime", "valueOf", "toISOString", "toJSON", "toString", scr_date_to_date_string_name, - scr_date_to_time_string_name, + scr_date_to_time_string_name, "toUTCString", "getFullYear", "getUTCFullYear", "getMonth", "getUTCMonth", "getDate", "getUTCDate", "getDay", "getUTCDay", "getHours", "getUTCHours", "getMinutes", "getUTCMinutes", "getSeconds", "getUTCSeconds", "getMilliseconds", - "getUTCMilliseconds", "getTimezoneOffset", + "getUTCMilliseconds", "getTimezoneOffset", "setTime", }; #define SCR_NATIVE_DATE_METHODS (sizeof scr_native_date_methods / sizeof scr_native_date_methods[0]) static SCR_TL ScrDyn *scr_native_date_method_values[SCR_NATIVE_DATE_METHODS]; @@ -13111,13 +13237,34 @@ static void scr_native_date_methods_cleanup(void) { } /* A Date method read as a value and called later: the closure's function - * slot holds the method's name, and the receiver must be a Date. */ + * slot holds the method's name, and the receiver must be a Date. On an + * instance of a class declared `extends Date` the method runs over its + * time value; toJSON calls the instance's toISOString. */ static ScrDyn *scr_native_date_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { const char *method = closure->fn; ScrDyn *self = scr_dyn_this_get(); ScrDyn *result = NULL; + double time; if (scr_dyn_native_date_is(self)) { result = scr_native_date_invoke(self->v.handle.ptr, self, method, args, argc, method); + } else if (scr_dyn_date_class_time(self, &time)) { + if (!strcmp(method, "toJSON")) { + result = isfinite(time) ? scr_date_class_call(SCR_DATE_CLASS_TO_ISO_STRING, self, NULL) + : scr_dyn_new_null(); + } else { + ScrNativeDate date = { 1, time }; + result = scr_native_date_invoke(&date, self, method, args, argc, method); + if (result && date.milliseconds != time && !(isnan(date.milliseconds) && isnan(time))) { + ScrDyn *stored = scr_dyn_new_num(date.milliseconds); + ScrDyn *set = scr_date_class_call(SCR_DATE_CLASS_SET_TIME, self, stored); + scr_dyn_release(stored); + if (!set) { + scr_dyn_release(result); + result = NULL; + } + scr_dyn_release(set); + } + } } else { static const char message[] = "this is not a Date object."; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); @@ -13126,6 +13273,21 @@ static ScrDyn *scr_native_date_method_call(ScrClosure *closure, ScrDyn *const *a return result; } +static ScrDyn *scr_native_date_get(void *ptr, const char *key, size_t key_len); + +/* The Date method `key` as a function value when `value` holds an instance + * of a class declared `extends Date`, or NULL. The class's own members are + * read first, by the caller. */ +static ScrDyn *scr_date_class_method(const ScrDyn *value, const char *key, size_t key_len) { + if (!scr_date_class_operation) return NULL; + bool named = false; + for (size_t i = 0; i < SCR_NATIVE_DATE_METHODS && !named; i++) + named = strlen(scr_native_date_methods[i]) == key_len && !memcmp(scr_native_date_methods[i], key, key_len); + double time; + if (!named || !scr_dyn_date_class_time(value, &time)) return NULL; + return scr_native_date_get(NULL, key, key_len); +} + /* Property reads of a Date: its methods as function values, one per name * and thread, and NULL (undefined) for other names. */ static ScrDyn *scr_native_date_get(void *ptr, const char *key, size_t key_len) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3974f346e6..2404c38859 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: 3.22.2 version: 3.22.2 js-yaml: - specifier: 4.3.0 - version: 4.3.0 + specifier: 4.3.2 + version: 4.3.2 oxfmt: specifier: 0.71.0 version: 0.71.0 @@ -41,6 +41,9 @@ importers: rollup: specifier: 4.62.2 version: 4.62.2 + smol-toml: + specifier: 1.7.1 + version: 1.7.1 three: specifier: 0.186.1 version: 0.186.1 @@ -2089,8 +2092,8 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true json-schema-to-ts@1.6.4: @@ -2458,6 +2461,10 @@ packages: resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==} engines: {node: '>= 18'} + smol-toml@1.7.1: + resolution: {integrity: sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==} + engines: {node: '>= 18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4305,7 +4312,7 @@ snapshots: dependencies: argparse: 2.0.1 - js-yaml@4.3.0: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 @@ -4694,6 +4701,8 @@ snapshots: smol-toml@1.5.2: {} + smol-toml@1.7.1: {} + source-map-js@1.2.1: {} srvx@0.11.16: {} diff --git a/tests/corpus/7040-es5-error-constructor.mjs b/tests/corpus/7040-es5-error-constructor.mjs new file mode 100644 index 0000000000..977567125e --- /dev/null +++ b/tests/corpus/7040-es5-error-constructor.mjs @@ -0,0 +1,79 @@ +// A constructor function that inherits from Error.prototype, the pre-class +// way of declaring an error type. +function ParseFailure(reason, position) { + Error.call(this); + this.name = "ParseFailure"; + this.reason = reason; + this.position = position; + this.message = reason + (position === undefined ? "" : " at " + position); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } else { + this.stack = new Error().stack || ""; + } +} +ParseFailure.prototype = Object.create(Error.prototype); +ParseFailure.prototype.constructor = ParseFailure; +ParseFailure.prototype.toString = function toString(compact) { + return this.name + ": " + this.reason + (compact ? "" : " (position " + this.position + ")"); +}; + +const failure = new ParseFailure("unexpected token", 7); +console.log(failure instanceof ParseFailure, failure instanceof Error, failure.constructor === ParseFailure); +console.log(failure.name, "|", failure.message, "|", failure.reason, failure.position); +console.log(failure.toString(), "|", failure.toString(true), "|", String(failure), "|", `${failure}`); +console.log(Object.keys(failure), JSON.stringify(failure)); +console.log(typeof failure.stack, failure.stack.split("\n")[0], Object.keys(failure).includes("stack")); +console.log(Object.prototype.toString.call(failure), Error.isError(failure)); + +try { + throw new ParseFailure("thrown"); +} catch (caught) { + console.log(caught instanceof Error, caught instanceof ParseFailure, caught.reason, caught.message); +} +const values = [new ParseFailure("in a list"), new Error("plain"), { name: "Fake", message: "x" }, "text"]; +console.log(values.map((value) => value instanceof Error)); + +// The inherited members of Error.prototype. +const bare = Object.create(Error.prototype); +console.log(bare instanceof Error, bare.name, JSON.stringify(bare.message), String(bare), Object.keys(bare)); +console.log(Error.prototype.name, JSON.stringify(Error.prototype.message), Error.prototype instanceof Error); +const record = Object.create(Error.prototype); +record.name = "N"; +record.message = "M"; +console.log(String(record), `${Object.create(record)}`); + +// captureStackTrace on other objects: the header follows name and message. +const target = { name: "Custom", message: "details", toString() { return "ignored"; } }; +Error.captureStackTrace(target); +console.log(target.stack.split("\n")[0], Object.keys(target)); +const empty = {}; +Error.captureStackTrace(empty); +console.log(empty.stack.split("\n")[0], typeof Error.captureStackTrace); + +// A subtype of the subtype. +function StrictFailure(reason) { + ParseFailure.call(this, reason, 0); + this.name = "StrictFailure"; +} +StrictFailure.prototype = Object.create(ParseFailure.prototype); +StrictFailure.prototype.constructor = StrictFailure; +const strict = new StrictFailure("strict mode"); +console.log(strict instanceof StrictFailure, strict instanceof ParseFailure, strict instanceof Error, strict.toString(true)); + +// A class that extends Error calls it too, directly or through `?.`. +class TokenError extends Error { + constructor(message) { + super(message); + this.name = "TokenError"; + Error.captureStackTrace(this, TokenError); + } +} +class LexError extends TokenError { + constructor(message) { + super(message), (this.name = "LexError"), Error.captureStackTrace?.(this, this.constructor); + } +} +for (const error of [new TokenError("bad token"), new LexError("bad char")]) { + console.log(error.name, error.message, error.stack.split("\n")[0], error instanceof TokenError, Object.keys(error)); +} diff --git a/tests/corpus/7041-error-called-without-new.mjs b/tests/corpus/7041-error-called-without-new.mjs new file mode 100644 index 0000000000..bf1df8e995 --- /dev/null +++ b/tests/corpus/7041-error-called-without-new.mjs @@ -0,0 +1,40 @@ +// The standard error constructors called as functions construct errors too, +// and Function.prototype.call ignores the receiver it is given. +const plain = Error("plain"); +const typed = TypeError("typed"); +const range = RangeError(); +const withCause = Error("wrapped", { cause: plain }); +console.log(plain instanceof Error, plain.message, String(plain)); +console.log(typed instanceof TypeError, typed instanceof Error, typed.name, typed.message); +console.log(range instanceof RangeError, JSON.stringify(range.message), String(range)); +console.log(withCause.cause === plain, withCause.message); +console.log(SyntaxError("s").name, ReferenceError("r").name, EvalError("e").name, URIError("u").name); + +const receiver = { marker: true }; +const called = Error.call(receiver, "via call"); +console.log(called instanceof Error, called.message, Object.keys(receiver), called === receiver); +const typedCall = TypeError.call(null, "typed via call"); +console.log(typedCall instanceof TypeError, typedCall.message); +const noArguments = Error.call(); +console.log(noArguments instanceof Error, JSON.stringify(noArguments.message)); + +let evaluated = 0; +function effect(value) { + evaluated++; + return value; +} +const ordered = Error.call(effect({}), effect("ordered")); +console.log(evaluated, ordered.message); + +function Wrapper(message) { + const base = Error.call(this, message); + this.message = base.message; + this.kind = "wrapper"; +} +const wrapped = new Wrapper("kept"); +console.log(wrapped.message, wrapped.kind, wrapped instanceof Wrapper, wrapped instanceof Error); +try { + throw Error("thrown without new"); +} catch (error) { + console.log(error instanceof Error, error.message); +} diff --git a/tests/corpus/7042-string-method-names-on-objects.mjs b/tests/corpus/7042-string-method-names-on-objects.mjs new file mode 100644 index 0000000000..c623f54171 --- /dev/null +++ b/tests/corpus/7042-string-method-names-on-objects.mjs @@ -0,0 +1,70 @@ +// Objects whose members share names with String.prototype methods, filled +// in after their declaration. A call dispatches on the receiver's runtime +// kind: a string runs the String method, any other value its own member. +var helpers = {}; +var initialized; +function setup() { + if (initialized) return helpers; + initialized = true; + function repeat(text, count) { + let out = ""; + for (let i = 0; i < count; i += 1) out += text; + return out; + } + function padStart(text, width, fill) { + let out = String(text); + while (out.length < width) out = fill + out; + return out; + } + helpers.repeat = repeat; + helpers.padStart = padStart; + helpers.trim = function (value) { + return "<" + value + ">"; + }; + helpers.split = (value, separator) => [value, separator, "own"]; + helpers.toUpperCase = () => "own upper"; + return helpers; +} + +const h = setup(); +console.log("[" + h.repeat("ab", 3) + "]", "[" + h.repeat("-", 0) + "]"); +console.log(h.padStart(7, 3, "0"), h.padStart("long", 2, "*")); +console.log(h.trim(" x "), h.split("a,b", ","), h.toUpperCase()); + +const indent = h.repeat(" ", 2); +console.log("[" + indent + "]", indent.length); +const lines = ["one", "two"].map((line, index) => h.padStart(index + 1, 3, " ") + " | " + line); +console.log(lines.join("\n")); + +// The same names on a missing member throw Node's TypeError. +var partial = {}; +function fillPartial() { + partial.present = true; +} +fillPartial(); +try { + partial.repeat("x", 2); +} catch (error) { + console.log(error instanceof TypeError, error.message); +} +try { + partial.trim(); +} catch (error) { + console.log(error instanceof TypeError, error.message); +} + +// Arguments are evaluated once, in order, after the member is read. +const order = []; +var traced = {}; +function fillTraced() { + traced.replace = (a, b) => { + order.push("call"); + return a + b; + }; +} +fillTraced(); +function arg(value) { + order.push("arg " + value); + return value; +} +console.log(traced.replace(arg(1), arg(2)), order.join(", ")); diff --git a/tests/corpus/7043-date-set-time.mjs b/tests/corpus/7043-date-set-time.mjs new file mode 100644 index 0000000000..a673c58dc4 --- /dev/null +++ b/tests/corpus/7043-date-set-time.mjs @@ -0,0 +1,28 @@ +// Date.prototype.setTime on Dates a JavaScript file constructs: the Date +// is one mutable object, so every reference sees the new time value. +function shifted(year, month, day, minutes) { + const date = new Date(Date.UTC(year, month, day, 12, 30, 15, 250)); + if (minutes) date.setTime(date.getTime() - minutes * 60000); + return date; +} +const unchanged = shifted(2001, 1, 3, 0); +const moved = shifted(2001, 1, 3, 90); +console.log(unchanged.toISOString(), moved.toISOString()); + +const alias = moved; +const record = { when: moved }; +console.log(moved.setTime(1000), alias.getTime(), record.when.toISOString()); +console.log(moved.setTime("2500"), moved.getTime(), moved.setTime(1.9), moved.setTime(-1.9)); +console.log(moved.setTime(NaN), moved.getTime(), String(moved), moved.toJSON()); +console.log(moved.setTime(8.64e15), moved.setTime(8.64e15 + 1), moved.setTime(-8.64e15 - 1)); +console.log(Object.is(moved.setTime(-0), 0), moved.setTime(), moved.setTime(true), moved.setTime(null)); +console.log(moved.setTime({ valueOf: () => 86400000 }), moved.toISOString()); +try { + moved.setTime({ valueOf: () => { throw new Error("no time"); } }); +} catch (error) { + console.log(error.message, moved.toISOString()); +} +console.log(JSON.stringify({ at: shifted(1999, 11, 31, 1) })); +const dates = [shifted(2020, 0, 1, 0), shifted(2020, 0, 1, 0)]; +dates.forEach((date, index) => date.setTime(date.getTime() + index * 1000)); +console.log(dates.map((date) => date.getUTCSeconds())); diff --git a/tests/corpus/7044-nested-empty-object-maps.mjs b/tests/corpus/7044-nested-empty-object-maps.mjs new file mode 100644 index 0000000000..e8d330f200 --- /dev/null +++ b/tests/corpus/7044-nested-empty-object-maps.mjs @@ -0,0 +1,46 @@ +// Object literals whose fields start as empty objects, used as maps the +// program adds keys to, including chained assignment into two of them. +function index(entries) { + const table = { + first: {}, + second: {}, + all: {}, + lists: { first: [], second: [], all: [] }, + }; + function add(entry) { + if (entry.many) { + table.lists[entry.group].push(entry.name); + table.lists["all"].push(entry.name); + } else { + table[entry.group][entry.name] = table["all"][entry.name] = entry; + } + } + entries.forEach(add); + return table; +} +const table = index([ + { group: "first", name: "a" }, + { group: "second", name: "b" }, + { group: "first", name: "c", many: true }, + { group: "second", name: "a" }, +]); +console.log(Object.keys(table.first), Object.keys(table.second), Object.keys(table.all), table.lists); +console.log(table.all.a === table.second.a, table.first.a.group, table.all.b.name, "c" in table.first); +console.log(JSON.stringify(table)); + +function counters(words) { + const result = { seen: {}, firstAt: {} }; + words.forEach((word, position) => { + result.seen[word] = (result.seen[word] || 0) + 1; + if (!(word in result.firstAt)) result.firstAt[word] = position; + }); + return result; +} +console.log(JSON.stringify(counters(["x", "y", "x", "z", "y", "x"]))); + +const settings = { values: {}, names: {} }; +const key = String(Math.round(1.6)); +settings.values[key] = 2; +settings["names"][key + "!"] = settings.values[key] * 10; +delete settings.values[key]; +console.log(JSON.stringify(settings), Object.keys(settings.values).length); diff --git a/tests/corpus/7045-instanceof-constructor-values.mjs b/tests/corpus/7045-instanceof-constructor-values.mjs new file mode 100644 index 0000000000..6b41cfdca5 --- /dev/null +++ b/tests/corpus/7045-instanceof-constructor-values.mjs @@ -0,0 +1,43 @@ +// `instanceof` against constructors held as values: a property holding a +// global constructor, and constructor functions with prototype chains. +const kinds = [ + { name: "Date", type: Date }, + { name: "RegExp", type: RegExp }, + { name: "Map", type: Map }, + { name: "Set", type: Set }, + { name: "WeakMap", type: WeakMap }, + { name: "Promise", type: Promise }, + { name: "Array", type: Array }, +]; +function classify(value) { + return kinds + .filter((kind) => typeof value === "object" && value !== null && value instanceof kind.type) + .map((kind) => kind.name) + .join("+") || "none"; +} +const samples = [new Date(0), /x/g, new Map([[1, 2]]), new Set([1]), new WeakMap(), Promise.resolve(1), [1, 2], { a: 1 }, null, "text", 4]; +console.log(samples.map(classify).join(" ")); + +function Shape(sides) { + this.sides = sides; +} +Shape.prototype.describe = function () { + return this.sides + " sides"; +}; +function Square() { + Shape.call(this, 4); +} +Square.prototype = Object.create(Shape.prototype); +Square.prototype.constructor = Square; +const square = new Square(); +const shape = new Shape(3); +const types = { Shape, Square }; +console.log(square instanceof types.Shape, square instanceof types.Square, shape instanceof types.Square); +console.log(square.describe(), shape.describe(), Object.create(Square.prototype) instanceof types.Shape); +console.log([square, shape, {}, []].map((value) => value instanceof types.Shape)); +const notCallable = JSON.parse("{}"); +try { + console.log(square instanceof notCallable); +} catch (error) { + console.log(error instanceof TypeError, error.message); +} diff --git a/tests/corpus/7046-parse-float-extra-arguments.mjs b/tests/corpus/7046-parse-float-extra-arguments.mjs new file mode 100644 index 0000000000..9046a91456 --- /dev/null +++ b/tests/corpus/7046-parse-float-extra-arguments.mjs @@ -0,0 +1,9 @@ +// parseFloat reads one argument; Node ignores any others. +const radix = 10; +const inputs = ["3.25", " -0.5e2xyz", "0x10", "Infinityx", ".5", "-.e1", "", "1_000"]; +console.log(inputs.map((text) => parseFloat(text, 10))); +console.log(inputs.map((text) => parseFloat(text, radix, "ignored"))); +const fromJson = JSON.parse('{"value":"12.5kg","n":7}'); +console.log(parseFloat(fromJson.value, 10), parseFloat(fromJson.n, radix), Object.is(parseFloat("-0", 10), -0)); +const values = ["1.5", "2.25"].map((text) => parseFloat(text.replace(/_/g, ""), 10)); +console.log(values, values[0] + values[1]); diff --git a/tests/corpus/7047-dynamic-string-index.mjs b/tests/corpus/7047-dynamic-string-index.mjs new file mode 100644 index 0000000000..fc81db3ce2 --- /dev/null +++ b/tests/corpus/7047-dynamic-string-index.mjs @@ -0,0 +1,21 @@ +// Indexing a string with a key whose type is only known at run time reads +// the property the key names: a code unit, the length, or undefined. +function skipSpaces(text, at) { + let c; + while ((c = text[at]) === " " || c === "\t") at++; + return [at, c]; +} +const where = JSON.parse('{"at":2,"name":"length","past":9,"text":"1","neg":-1,"frac":1.5}'); +console.log(skipSpaces("ab \tcd", where.at), skipSpaces("ab", where.past)); +const s = "hello"; +console.log(s[where.at], s[where.name], s[where.past], s[where.text], s[where.neg], s[where.frac]); +console.log(typeof s[where.past], typeof s[where.name], s[where.at] === "l"); +function scan(text, start) { + let out = ""; + for (let i = start; text[i] !== undefined && text[i] !== ";"; i++) out += text[i]; + return out; +} +console.log(scan("key=value;rest", JSON.parse("4")), scan("abc", JSON.parse("1"))); +const codes = []; +for (let i = JSON.parse("0"); i < s.length; i++) codes.push(s[i].charCodeAt(0)); +console.log(codes); diff --git a/tests/corpus/7048-unary-number-coercions.mjs b/tests/corpus/7048-unary-number-coercions.mjs new file mode 100644 index 0000000000..4154ae79e6 --- /dev/null +++ b/tests/corpus/7048-unary-number-coercions.mjs @@ -0,0 +1,34 @@ +// Unary plus and minus on booleans and on values that may be strings or +// undefined, and += on a binding that holds a number or a string. +function step(text, at) { + const open = text[at] === "["; + at += +open; + return [at, +open + +true + +false, -open]; +} +const at = JSON.parse("1"); +console.log(step("a[b", at), step("ab", at)); +let total = 0; +for (const flag of [true, false, true]) total += +flag; +console.log(total, +true, -true, +(1 > 2), -(2 > 1)); + +const match = "2024-01-02".match(/^(\d+)-(\d+)-(\d+)(?:T(\d+))?$/); +console.log(+match[1], +match[4], -match[2], +match[3] + 1, typeof +match[4], -match[4]); +const optional = [undefined, "7", " 8 ", "", "x"]; +console.log(optional.map((value) => +value), optional.map((value) => -value)); + +let fraction = 0; +const digits = "5"; +if (digits) { + fraction = digits.slice(0, 3); + while (fraction.length < 3) fraction += "0"; + fraction = +fraction; +} +console.log(fraction, typeof fraction); +let width = 0; +width = digits.repeat(2); +width += 1; +console.log(width, typeof width); +let label = "x"; +label += 5; +label += true; +console.log(label); diff --git a/tests/corpus/7049-member-assignment-values.mjs b/tests/corpus/7049-member-assignment-values.mjs new file mode 100644 index 0000000000..b614ba3083 --- /dev/null +++ b/tests/corpus/7049-member-assignment-values.mjs @@ -0,0 +1,20 @@ +// Member assignments used as values: the receiver, the key and the value are +// evaluated in order, and the expression yields the assigned value. +const log = []; +const holder = { get inner() { log.push("receiver"); return this._inner; }, _inner: {} }; +function value(v) { log.push("value"); return v; } +function key(k) { log.push("key"); return k; } +const r = holder.inner[key("k")] = value(5); +console.log(r, log.join(","), holder._inner.k); +const store = JSON.parse('{"a":{},"b":{}}'); +const t = { id: "x" }; +const out = store["a"][t.id] = store.b[t.id] = t; +console.log(out === t, JSON.stringify(store)); +let n; +const arr = [0, 0, 0]; +n = arr[1] = arr[2] = 7; +console.log(n, arr); +function Counter() { this.count = 0; } +Counter.prototype.bump = function () { return (this.count = this.count + 1); }; +const c = new Counter(); +console.log(c.bump(), c.bump(), c.count); diff --git a/tests/corpus/7050-date-subclass.mjs b/tests/corpus/7050-date-subclass.mjs new file mode 100644 index 0000000000..c22250fc13 --- /dev/null +++ b/tests/corpus/7050-date-subclass.mjs @@ -0,0 +1,69 @@ +// A class that extends Date: private fields beside the time value, super() +// with each Date constructor form, an overriding toISOString that +// JSON.stringify reaches through Date.prototype.toJSON, and the inherited +// Date methods. +class Moment extends Date { + #zone = "Z"; + #precise = true; + constructor(value, zone) { + if (value === undefined) super(0); + else super(value); + if (zone !== undefined) this.#zone = zone; + } + get zone() { + return this.#zone; + } + coarse() { + this.#precise = false; + return this; + } + toISOString() { + const iso = super.toISOString(); + const base = this.#precise ? iso : iso.slice(0, 19) + "Z"; + return this.#zone === "Z" ? base : base.slice(0, -1) + this.#zone; + } +} + +const fromString = new Moment("2001-02-03T04:05:06.789Z"); +const fromNumber = new Moment(86400000, "+01:00"); +const fromDate = new Moment(new Date(Date.UTC(1999, 11, 31, 23, 59, 59))); +const fromMoment = new Moment(fromString); +const none = new Moment(); +console.log(fromString.toISOString(), fromNumber.toISOString(), fromDate.toISOString()); +console.log(fromMoment.getTime() === fromString.getTime(), none.getTime(), fromNumber.zone); +console.log(fromString.getUTCFullYear(), fromString.getUTCMonth(), fromString.getUTCDate(), fromString.getUTCHours(), fromString.getUTCMilliseconds()); +console.log(fromString instanceof Date, fromString instanceof Moment, new Date(0) instanceof Moment); +console.log(JSON.stringify({ fromString, fromNumber, list: [fromDate.coarse()] })); +console.log(fromNumber.toJSON(), JSON.stringify(fromNumber)); +console.log(String(fromDate).slice(0, 15), `${fromDate}`.slice(0, 15), fromDate.toUTCString()); +console.log(+fromNumber, +fromNumber - 1000, fromNumber.valueOf(), fromNumber + "" === String(fromNumber)); +console.log(fromString > fromNumber, fromString < fromNumber, fromString >= fromMoment); +console.log(fromNumber.setTime(1000), fromNumber.getTime(), fromNumber.toISOString()); +console.log(new Date(fromString).toISOString(), new Date(fromNumber).getTime()); +console.log(fromString, [fromNumber], { d: fromDate }); + +const invalid = new Moment("not a date"); +console.log(Number.isNaN(invalid.getTime()), JSON.stringify({ invalid }), invalid.toJSON(), String(invalid)); +console.log(invalid); +try { + invalid.toISOString(); +} catch (error) { + console.log(error instanceof RangeError, error.message); +} + +class LocalMoment extends Moment { + constructor(value) { + super(value, "local"); + } + toISOString() { + return super.toISOString().replace("local", ""); + } +} +const local = new LocalMoment(0); +console.log(local.toISOString(), JSON.stringify([local]), local instanceof Moment, local.zone); + +// The field constructor and Date's statics, which the subclass inherits. +class Plain extends Date {} +const fields = new Plain(2020, 0, 15, 10, 30); +console.log(fields.getFullYear(), fields.getMonth(), fields.getDate(), fields.getHours(), fields.getMinutes()); +console.log(Plain.UTC(2000, 0, 1), Plain.parse("2000-01-01T00:00:00Z"), typeof Plain.now(), Object.keys(fields)); diff --git a/tests/corpus/7051-date-subclass-typed.ts b/tests/corpus/7051-date-subclass-typed.ts new file mode 100644 index 0000000000..e39b5f5341 --- /dev/null +++ b/tests/corpus/7051-date-subclass-typed.ts @@ -0,0 +1,47 @@ +// A TypeScript class that extends Date, and a subclass of it: typed calls +// of the inherited Date methods, conversions, comparisons, JSON and +// console output, with the toISOString override visible to toJSON. +class Stamp extends Date { + #precise: boolean; + label = "stamp"; + constructor(value: number | string, precise = false) { + super(value); + this.#precise = precise; + } + override toISOString(): string { + const iso = super.toISOString(); + return this.#precise ? iso : iso.slice(0, 19) + "Z"; + } + shifted(ms: number): Stamp { + return new Stamp(this.getTime() + ms, this.#precise); + } +} + +class Tick extends Stamp { + constructor(value: number) { + super(value, true); + } + seconds(): number { + return this.getUTCSeconds() + this.getUTCMilliseconds() / 1000; + } +} + +const a = new Stamp("2001-02-03T04:05:06.789Z"); +const b = new Tick(1500); +console.log(a.toISOString(), b.toISOString(), a.getTime(), b.seconds()); +console.log(a instanceof Date, b instanceof Stamp, b instanceof Date, a.shifted(1000).toISOString()); +console.log(JSON.stringify({ a, b, list: [a] }), JSON.stringify(a), a.toJSON(), b.toJSON()); +console.log(String(a).slice(0, 15), `${b}`.slice(0, 15), +a, b.valueOf(), a.getTime() - b.getTime()); +console.log(a < b, a > b, a >= b, a == a, a + "!" === String(a) + "!"); +console.log(a); +console.log([b], { b }); +console.log(a.setTime(5000), a.getTime(), a.toISOString(), a.label); + +const items: Stamp[] = [a, b, new Tick(0)]; +items.sort((x, y) => x.getTime() - y.getTime()); +console.log(items.map((s) => s.toISOString()).join(",")); +console.log(items.filter((s) => s instanceof Tick).length); +function describe(value: Stamp | string): string { + return typeof value === "string" ? value : value.toISOString(); +} +console.log(describe(b), describe("text")); diff --git a/tests/corpus/7052-date-subclass-dynamic.mjs b/tests/corpus/7052-date-subclass-dynamic.mjs new file mode 100644 index 0000000000..267c01ded1 --- /dev/null +++ b/tests/corpus/7052-date-subclass-dynamic.mjs @@ -0,0 +1,43 @@ +// Instances of a class that extends Date held in values typed only at run +// time: method calls, method values, conversions and instanceof through +// those values, and JSON and console output of the containers. +class Day extends Date { + #label; + constructor(value, label) { + super(value); + this.#label = label; + } + get label() { + return this.#label; + } + toISOString() { + return super.toISOString().slice(0, 10); + } +} +class Plain extends Date {} + +const box = JSON.parse('{"items":[],"byName":{}}'); +box.items.push(new Day(Date.UTC(2020, 1, 29), "leap"), new Plain(0), new Date(1000), "text"); +box.byName.first = box.items[0]; + +function inspect(value) { + if (!(value instanceof Date)) return "not a date: " + value; + return [value.toISOString(), value.getTime(), value.getUTCDate(), value instanceof Day, value.toJSON()].join(" | "); +} +for (const item of box.items) console.log(inspect(item)); +console.log(JSON.stringify(box)); +console.log(box.byName.first.label, box.byName.first.getUTCFullYear(), String(box.items[1]).slice(0, 15)); +console.log(+box.items[0], box.items[0] - box.items[1], box.items[0] > box.items[2], `${box.items[1]}`.slice(0, 3)); + +const getTime = box.items[0].getTime; +console.log(getTime.call(box.items[1]), getTime.call(box.items[0]), typeof getTime); +const toISO = box.items[0].toISOString; +console.log(toISO.call(box.items[0])); +console.log(box.items[1].setTime(5000), box.items[1].getTime(), box.items[1].toISOString()); +try { + getTime.call({}); +} catch (error) { + console.log(error instanceof TypeError); +} +console.log(box.items); +console.log(new Date(box.items[0]).toISOString(), new Date(box.items[1]).getTime()); diff --git a/tests/corpus/7053-private-fields-untyped-receivers.mjs b/tests/corpus/7053-private-fields-untyped-receivers.mjs new file mode 100644 index 0000000000..815f612b07 --- /dev/null +++ b/tests/corpus/7053-private-fields-untyped-receivers.mjs @@ -0,0 +1,72 @@ +// Private fields read and written through receivers whose type is known only +// at run time: static factories and helpers that take the instance as an +// argument, compound assignments and updates, `#x in value`, subclasses, and +// Node's TypeErrors for values that do not carry the field. +class Counter { + #count = 0; + #label = null; + constructor(label) { + this.#label = label; + } + static labeled(label, start) { + let counter = new Counter("pending"); + counter.#label = label; + counter.#count = start; + return counter; + } + static read(counter) { + return [counter.#label, counter.#count]; + } + static bump(counter, by) { + counter.#count += by; + counter.#count++; + ++counter.#count; + return counter.#count; + } + static clear(counter) { + counter.#label = null; + return counter; + } + static owns(value) { + return #count in value; + } + describe() { + return `${this.#label}:${this.#count}`; + } +} +class LimitedCounter extends Counter { + #limit = 3; + static limit(value) { + return value.#limit; + } +} + +const made = Counter.labeled("made", 5); +console.log(made.describe(), Counter.read(made), Counter.bump(made, 10), made.describe()); +const held = JSON.parse('{"list":[]}'); +held.list.push(new Counter("held"), new LimitedCounter("limited")); +for (const item of held.list) console.log(Counter.read(item), Counter.bump(item, 1), item.describe(), Counter.owns(item)); +console.log(LimitedCounter.limit(held.list[1]), Counter.clear(held.list[0]).describe()); +for (const value of [{}, [], null, undefined, 1, "text", held.list[0]]) { + try { + console.log(Counter.read(value)); + } catch (error) { + console.log("read", error instanceof TypeError, error.message); + } + try { + Counter.bump(value, 1); + console.log("bumped"); + } catch (error) { + console.log("bump", error instanceof TypeError, error.message); + } + try { + console.log(LimitedCounter.limit(value)); + } catch (error) { + console.log("limit", error instanceof TypeError, error.message); + } + try { + console.log("owns", Counter.owns(value)); + } catch (error) { + console.log("owns", error instanceof TypeError, error.message); + } +} diff --git a/tests/corpus/7054-let-bindings-with-null-writes.mjs b/tests/corpus/7054-let-bindings-with-null-writes.mjs new file mode 100644 index 0000000000..c8ca26a9d9 --- /dev/null +++ b/tests/corpus/7054-let-bindings-with-null-writes.mjs @@ -0,0 +1,37 @@ +// `let` bindings initialized with a string, number or boolean and later +// assigned values that can be null or undefined. JavaScript files are +// checked without strict null checks, so the declared type of each binding +// is the primitive alone. +const PATTERN = /^(\d+)(?:\.(\d+))?([+-]\d\d:\d\d)?$/; +function parts(text) { + let offset = "Z"; + let fraction = 0; + let signed = false; + const match = text.match(PATTERN); + if (match) { + offset = match[3] || null; + fraction = match[2] ? +match[2] : undefined; + signed = match[3] ? match[3][0] === "-" : null; + } + return [offset, fraction, signed]; +} +console.log(parts("12"), parts("12.5"), parts("12.25-01:00"), parts("x")); + +function pick(flag) { + let name = "default"; + name = flag ? name.toUpperCase() : void 0; + let count = 1; + count = flag && count + 1; + let ready = true; + ready = flag === undefined ? undefined : !flag; + return [name, count, ready, typeof name, typeof count]; +} +console.log(pick(true), pick(false), pick(undefined)); + +let last = "start"; +const seen = []; +for (const value of ["a", null, "b", undefined]) { + last = value ?? null; + seen.push(last === null ? "null" : last.length); +} +console.log(seen, last); diff --git a/tests/fixtures/npm-static/js-yaml-cli.mjs b/tests/fixtures/npm-static/js-yaml-cli.mjs new file mode 100644 index 0000000000..8c958cf808 --- /dev/null +++ b/tests/fixtures/npm-static/js-yaml-cli.mjs @@ -0,0 +1,63 @@ +// The published js-yaml package compiled as static modules: loading, dumping, +// schemas, custom types and the YAMLException errors it throws. +import yaml from "js-yaml"; +const text = ` +# config +name: demo +version: 1.2 +count: 0x1F +octal: 0o17 +neg: -12_000 +flt: 6.02e+23 +inf: .inf +nan: .NaN +on: yes +off: false +nil: ~ +date: 2001-12-14 +stamp: 2001-12-14t21:59:43.10-05:00 +str: "quoted \\u263A \\t tab" +single: 'it''s' +multi: | + line one + line two +folded: >- + folded + text +base: &base { a: 1, b: [x, y] } +derived: + <<: *base + c: 3 +list: + - one + - { k: v } + - [1, 2, 3] + - !!str 123 +set: !!set { a, b } +omap: !!omap [ { x: 1 }, { y: 2 } ] +binary: !!binary aGVsbG8= +`; +const doc = yaml.load(text); +console.log(JSON.stringify(doc, (k, v) => (v instanceof Uint8Array ? Array.from(v) : v))); +console.log(doc.stamp instanceof Date, doc.stamp.toISOString(), doc.date.toISOString(), Object.is(doc.nan, NaN) || Number.isNaN(doc.nan)); +const docs = yaml.loadAll("---\na: 1\n---\nb: 2\n...\n"); +console.log(JSON.stringify(docs)); +const items = []; +yaml.loadAll("--- 1\n--- two\n", (d) => items.push(d)); +console.log(items); +console.log(yaml.dump(doc, { sortKeys: true, lineWidth: 40, skipInvalid: true })); +console.log(yaml.dump({ a: "multi\nline\n", b: " lead", c: "yes", d: "123", e: null, f: [], g: {}, h: "x: y", i: -0, j: 1e21 }, { indent: 4 })); +console.log(yaml.dump([{ deep: { er: [1, { z: true }] } }], { flowLevel: 2 })); +console.log(yaml.dump({ s: "a'b\"c", u: "é☺", long: "word ".repeat(30).trim() }, { quotingType: '"', forceQuotes: true })); +for (const bad of ["a: [1", "a: b: c", "- a\nb: 1", "key: 'open", "a: !!int x", "? a\n? a\n", "&x a: *y"]) { + try { yaml.load(bad); console.log("no error", bad); } + catch (e) { console.log(e.name, "|", e.reason, "|", e.mark && e.mark.line, e.mark && e.mark.column, "|", e.message.split("\n")[0]); console.log(e.toString(true)); console.log(String(e).split("\n").length); } +} +try { yaml.load("a: 1\na: 2"); } catch (e) { console.log(e instanceof yaml.YAMLException, e instanceof Error, e.message); } +console.log(JSON.stringify(yaml.load("a: 1\na: 2", { json: true }))); +console.log(JSON.stringify(yaml.load("x: 1", { schema: yaml.JSON_SCHEMA })), JSON.stringify(yaml.load("x: yes", { schema: yaml.FAILSAFE_SCHEMA })), JSON.stringify(yaml.load("x: yes", { schema: yaml.CORE_SCHEMA }))); +const Point = new yaml.Type("!point", { kind: "sequence", construct: (d) => ({ x: d[0], y: d[1] }), instanceOf: Object, represent: (p) => [p.x, p.y] }); +const schema = yaml.DEFAULT_SCHEMA.extend([Point]); +console.log(JSON.stringify(yaml.load("p: !point [3, 4]", { schema }))); +try { yaml.dump({ f() {} }); } catch (e) { console.log(e.name, e.message); } +console.log(yaml.dump({ f() {}, u: undefined, k: 1 }, { skipInvalid: true })); diff --git a/tests/fixtures/npm-static/smol-toml-cli.mjs b/tests/fixtures/npm-static/smol-toml-cli.mjs new file mode 100644 index 0000000000..453e16801a --- /dev/null +++ b/tests/fixtures/npm-static/smol-toml-cli.mjs @@ -0,0 +1,68 @@ +// The published smol-toml package compiled as static modules: parsing every +// value kind, the TomlDate subclass of Date, stringify and TomlError. +import { parse, stringify, TomlDate, TomlError } from "smol-toml"; +const text = ` +# a document +title = "TOML \\"example\\"" +int = +99 +neg = -17 +hex = 0xDEADBEEF +oct = 0o755 +bin = 0b1101 +under = 1_000_000 +float = 6.626e-34 +inf = inf +nan = nan +bool = true +odt1 = 1979-05-27T07:32:00Z +odt2 = 1979-05-27T00:32:00-07:00 +odt3 = 1979-05-27T00:32:00.999999-07:00 +ldt = 1979-05-27T07:32:00 +ld = 1979-05-27 +lt = 07:32:00.123 +space = 1979-05-27 07:32:00Z +literal = 'C:\\Users\\nodejs\\templates' +multi = """ +Roses are red +Violets are blue""" +lmulti = ''' +The first newline is +trimmed in raw strings.''' + +[owner] +name = "Tom" +"quoted key" = 1 +dotted.inner.key = "x" + +[database] +ports = [ 8000, 8001, 8002 ] +data = [ ["delta", "phi"], [3.14] ] +temp_targets = { cpu = 79.5, case = 72.0 } + +[[products]] +name = "Hammer" +sku = 738594937 + +[[products]] + +[[products]] +name = "Nail" +color = "gray" +`; +const doc = parse(text); +console.log(JSON.stringify(doc, null, 1)); +for (const key of ["odt1", "odt2", "odt3", "ldt", "ld", "lt", "space"]) { + const d = doc[key]; + console.log(key, d instanceof Date, d instanceof TomlDate, d.isDateTime(), d.isLocal(), d.isDate(), d.isTime(), d.isValid(), d.toISOString(), Number.isNaN(d.getTime()) ? "NaN" : d.getTime()); +} +console.log(Number.isNaN(doc.nan), doc.inf, typeof doc.hex, doc.under); +console.log(stringify(doc)); +console.log(stringify({ a: new Date(0), b: TomlDate.wrapAsLocalDate(new Date(86400000)), c: new TomlDate("10:20:30"), n: [1, [2, 3]], t: { u: { v: "w" } }, s: "quote\" and \\ backslash", f: 1.5, i: 2, big: 2n ** 40n })); +const d = new TomlDate("2020-02-29T12:00:00+01:00"); +console.log(d.toISOString(), JSON.stringify({ d }), String(d.getUTCHours()), d.valueOf()); +const bad = new TomlDate("not a date"); +console.log(bad.isValid(), Number.isNaN(bad.getTime())); +for (const src of ["a = ", "a = 1\na = 2", "[t]\n[t]", "x = [1,", "k = 1979-13-01", "s = \"unterminated", "= 1", "a = 25:00:00"]) { + try { parse(src); console.log("parsed", JSON.stringify(src)); } + catch (e) { console.log(e instanceof TomlError, e instanceof Error, e.name, e.line, e.column, e.message.split("\n")[0]); } +} diff --git a/tests/harness/corpus-costs/7040-es5-error-constructor.mjs.json b/tests/harness/corpus-costs/7040-es5-error-constructor.mjs.json new file mode 100644 index 0000000000..40dc942dce --- /dev/null +++ b/tests/harness/corpus-costs/7040-es5-error-constructor.mjs.json @@ -0,0 +1 @@ +{ "plain": 3648, "san": 32315 } diff --git a/tests/harness/corpus-costs/7041-error-called-without-new.mjs.json b/tests/harness/corpus-costs/7041-error-called-without-new.mjs.json new file mode 100644 index 0000000000..d5a7a927c8 --- /dev/null +++ b/tests/harness/corpus-costs/7041-error-called-without-new.mjs.json @@ -0,0 +1 @@ +{ "plain": 2293, "san": 3259 } diff --git a/tests/harness/corpus-costs/7042-string-method-names-on-objects.mjs.json b/tests/harness/corpus-costs/7042-string-method-names-on-objects.mjs.json new file mode 100644 index 0000000000..e85a6cc3ea --- /dev/null +++ b/tests/harness/corpus-costs/7042-string-method-names-on-objects.mjs.json @@ -0,0 +1 @@ +{ "plain": 2776, "san": 20012 } diff --git a/tests/harness/corpus-costs/7043-date-set-time.mjs.json b/tests/harness/corpus-costs/7043-date-set-time.mjs.json new file mode 100644 index 0000000000..cdb4d4f92b --- /dev/null +++ b/tests/harness/corpus-costs/7043-date-set-time.mjs.json @@ -0,0 +1 @@ +{ "plain": 2423, "san": 3371 } diff --git a/tests/harness/corpus-costs/7044-nested-empty-object-maps.mjs.json b/tests/harness/corpus-costs/7044-nested-empty-object-maps.mjs.json new file mode 100644 index 0000000000..a7951b37ee --- /dev/null +++ b/tests/harness/corpus-costs/7044-nested-empty-object-maps.mjs.json @@ -0,0 +1 @@ +{ "plain": 2671, "san": 3931 } diff --git a/tests/harness/corpus-costs/7045-instanceof-constructor-values.mjs.json b/tests/harness/corpus-costs/7045-instanceof-constructor-values.mjs.json new file mode 100644 index 0000000000..1776c882da --- /dev/null +++ b/tests/harness/corpus-costs/7045-instanceof-constructor-values.mjs.json @@ -0,0 +1 @@ +{ "plain": 2726, "san": 4330 } diff --git a/tests/harness/corpus-costs/7046-parse-float-extra-arguments.mjs.json b/tests/harness/corpus-costs/7046-parse-float-extra-arguments.mjs.json new file mode 100644 index 0000000000..2e8f439530 --- /dev/null +++ b/tests/harness/corpus-costs/7046-parse-float-extra-arguments.mjs.json @@ -0,0 +1 @@ +{ "plain": 2204, "san": 3269 } diff --git a/tests/harness/corpus-costs/7047-dynamic-string-index.mjs.json b/tests/harness/corpus-costs/7047-dynamic-string-index.mjs.json new file mode 100644 index 0000000000..d73f3ccccd --- /dev/null +++ b/tests/harness/corpus-costs/7047-dynamic-string-index.mjs.json @@ -0,0 +1 @@ +{ "plain": 2451, "san": 3411 } diff --git a/tests/harness/corpus-costs/7048-unary-number-coercions.mjs.json b/tests/harness/corpus-costs/7048-unary-number-coercions.mjs.json new file mode 100644 index 0000000000..7107834518 --- /dev/null +++ b/tests/harness/corpus-costs/7048-unary-number-coercions.mjs.json @@ -0,0 +1 @@ +{ "plain": 2478, "san": 3675 } diff --git a/tests/harness/corpus-costs/7049-member-assignment-values.mjs.json b/tests/harness/corpus-costs/7049-member-assignment-values.mjs.json new file mode 100644 index 0000000000..551cfbf7bf --- /dev/null +++ b/tests/harness/corpus-costs/7049-member-assignment-values.mjs.json @@ -0,0 +1 @@ +{ "plain": 2456, "san": 3617 } diff --git a/tests/harness/corpus-costs/7050-date-subclass.mjs.json b/tests/harness/corpus-costs/7050-date-subclass.mjs.json new file mode 100644 index 0000000000..e212897357 --- /dev/null +++ b/tests/harness/corpus-costs/7050-date-subclass.mjs.json @@ -0,0 +1 @@ +{ "plain": 2932, "san": 3727 } diff --git a/tests/harness/corpus-costs/7051-date-subclass-typed.ts.json b/tests/harness/corpus-costs/7051-date-subclass-typed.ts.json new file mode 100644 index 0000000000..fad2824860 --- /dev/null +++ b/tests/harness/corpus-costs/7051-date-subclass-typed.ts.json @@ -0,0 +1 @@ +{ "plain": 2533, "san": 3429 } diff --git a/tests/harness/corpus-costs/7052-date-subclass-dynamic.mjs.json b/tests/harness/corpus-costs/7052-date-subclass-dynamic.mjs.json new file mode 100644 index 0000000000..cabbe475e9 --- /dev/null +++ b/tests/harness/corpus-costs/7052-date-subclass-dynamic.mjs.json @@ -0,0 +1 @@ +{ "plain": 2740, "san": 3689 } diff --git a/tests/harness/corpus-costs/7053-private-fields-untyped-receivers.mjs.json b/tests/harness/corpus-costs/7053-private-fields-untyped-receivers.mjs.json new file mode 100644 index 0000000000..8ebe6846ab --- /dev/null +++ b/tests/harness/corpus-costs/7053-private-fields-untyped-receivers.mjs.json @@ -0,0 +1 @@ +{ "plain": 2415, "san": 3338 } diff --git a/tests/harness/corpus-costs/7054-let-bindings-with-null-writes.mjs.json b/tests/harness/corpus-costs/7054-let-bindings-with-null-writes.mjs.json new file mode 100644 index 0000000000..d313a64f10 --- /dev/null +++ b/tests/harness/corpus-costs/7054-let-bindings-with-null-writes.mjs.json @@ -0,0 +1 @@ +{ "plain": 2293, "san": 3012 } diff --git a/tests/harness/npm-static-costs.json b/tests/harness/npm-static-costs.json index 2f1cdde1e3..1546f8d571 100644 --- a/tests/harness/npm-static-costs.json +++ b/tests/harness/npm-static-costs.json @@ -84,6 +84,8 @@ "the published Effect Function subpath initializes and runs statically": 9097, "the published Effect generator has deterministic native results": 105577, "the published Effect generator logs and completes statically": 112814, + "the published js-yaml loads, dumps and reports errors statically and matches Node": 12728, + "the published smol-toml parses and stringifies statically and matches Node": 11649, "untyped package methods preserve virtual overrides (llvm)": 3065 }, "san": { @@ -171,6 +173,8 @@ "the published Effect Function subpath initializes and runs statically": 8280, "the published Effect generator has deterministic native results": 136502, "the published Effect generator logs and completes statically": 181378, + "the published js-yaml loads, dumps and reports errors statically and matches Node": 35621, + "the published smol-toml parses and stringifies statically and matches Node": 25130, "untyped package methods preserve virtual overrides (llvm)": 2730 } } diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index df82efbcf2..7ee76307fd 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -1175,6 +1175,38 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { effectBuildTimeout, ); + test("the published js-yaml loads, dumps and reports errors statically and matches Node", async () => { + const entry = join(pilotRoot, "js-yaml-cli.mjs"); + const { coverage } = analyze(entry, { npmStatic: ["js-yaml"] }); + expect(coverage.npmStatic).toEqual([{ package: "js-yaml", status: "static" }]); + expect(coverage.diagnostics).toHaveLength(0); + const binary = await buildStatic(entry, ["js-yaml"]); + const [reference, native] = await Promise.all([ + runBinary(process.execPath, [entry]), + runBinary(binary, []), + ]); + expect(native.stdout.toString()).toBe(reference.stdout.toString()); + expect(comparableStderr(native.stderr)).toEqual(reference.stderr); + expect(native.exitCode).toBe(reference.exitCode); + expect(native.exitCode).toBe(0); + }, 300_000); + + test("the published smol-toml parses and stringifies statically and matches Node", async () => { + const entry = join(pilotRoot, "smol-toml-cli.mjs"); + const { coverage } = analyze(entry, { npmStatic: ["smol-toml"] }); + expect(coverage.npmStatic).toEqual([{ package: "smol-toml", status: "static" }]); + expect(coverage.diagnostics).toHaveLength(0); + const binary = await buildStatic(entry, ["smol-toml"]); + const [reference, native] = await Promise.all([ + runBinary(process.execPath, [entry]), + runBinary(binary, []), + ]); + expect(native.stdout.toString()).toBe(reference.stdout.toString()); + expect(comparableStderr(native.stderr)).toEqual(reference.stderr); + expect(native.exitCode).toBe(reference.exitCode); + expect(native.exitCode).toBe(0); + }, 300_000); + test("inferred variadic factories preserve checked callable results", async () => { const entry = join(pilotRoot, "variadic-factory-cli.ts"); const binary = await buildStatic(entry, ["variadic-factory"]); From 42d4ca27f9733f3f64b3d0909bc6042d71a8351c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 23:40:42 -0500 Subject: [PATCH 054/395] Run Promise.all of never-suspending async calls in place from stackless async bodies --- packages/compiler/src/backend/llvm/emitter.ts | 215 ++++++++++++++++++ .../compiler/src/backend/llvm/expr-async.ts | 6 + .../compiler/test/await-call-emission.test.ts | 45 ++++ .../7100-promise-all-inline-order.ts.json | 6 + ...7101-promise-all-inline-sync-throw.ts.json | 6 + .../7102-promise-all-inline-kinds.ts.json | 6 + tests/corpus/7100-promise-all-inline-order.ts | 86 +++++++ .../7101-promise-all-inline-sync-throw.ts | 21 ++ tests/corpus/7102-promise-all-inline-kinds.ts | 69 ++++++ .../7100-promise-all-inline-order.ts.json | 1 + ...7101-promise-all-inline-sync-throw.ts.json | 1 + .../7102-promise-all-inline-kinds.ts.json | 1 + 12 files changed, 463 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7100-promise-all-inline-order.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7101-promise-all-inline-sync-throw.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7102-promise-all-inline-kinds.ts.json create mode 100644 tests/corpus/7100-promise-all-inline-order.ts create mode 100644 tests/corpus/7101-promise-all-inline-sync-throw.ts create mode 100644 tests/corpus/7102-promise-all-inline-kinds.ts create mode 100644 tests/harness/corpus-costs/7100-promise-all-inline-order.ts.json create mode 100644 tests/harness/corpus-costs/7101-promise-all-inline-sync-throw.ts.json create mode 100644 tests/harness/corpus-costs/7102-promise-all-inline-kinds.ts.json diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 0bdbe23b2b..b306674fc6 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -287,6 +287,7 @@ import { import { arrNewCall, boxAccess, + elemAccess, boxNewCall, boxReleaseSym, computeTraced, @@ -571,6 +572,33 @@ function mentionsLocal(e: IrExpr, localId: string): boolean { return found; } +/** The IR nodes in `body` that name local `id`: its declaration, reads, + * writes and closure captures. */ +function countLocalMentions(body: readonly IrStmt[], id: string): number { + let count = 0; + const visit = (node: unknown): void => { + if (node === null || typeof node !== "object") return; + if (Array.isArray(node)) { + for (const item of node) visit(item); + return; + } + const record = node as Record; + if (record["localId"] === id) count++; + const captures = record["captures"]; + if (Array.isArray(captures)) + for (const c of captures) + if ( + c === id || + (typeof c === "object" && c !== null && (c as { localId?: unknown }).localId === id) + ) + count++; + for (const [key, value] of Object.entries(record)) + if (key !== "type" && key !== "loc" && key !== "captures") visit(value); + }; + visit(body); + return count; +} + export class LlEmitter { localArrayReads = new Map(); loopArrayBorrows: ReadonlySet = new Set(); @@ -864,6 +892,9 @@ export class LlEmitter { /** The async functions whose bodies never suspend and that a direct * await may enter in place (selectStacklessAsync). */ private inlineAwaitable = new Set(); + /** Locals whose array literal emitStmts left for the `await Promise.all` + * that reads them (deferredPromiseAllLocal). */ + private readonly deferredPromiseAllEntries = new Map>(); /** The promise a `return` emitted past its finally regions already * produced for its adopting await (borrowed from the return's parked * slot); that await takes it instead of evaluating its operand. */ @@ -1945,6 +1976,185 @@ export class LlEmitter { this.emitPendingCheck(); return value; } + /** The entries of `Promise.all(arg)` that a stackless coroutine body's + * await may run in place (emitCoroInlinePromiseAll): `arg` is a non-empty + * array literal, or the local whose declaration emitStmts deferred to this + * await (deferredPromiseAllEntries), whose every entry is a call to a + * never-suspending async function, and the result is an array. Null + * otherwise. Consumes a deferred local's entries. */ + inlinePromiseAllEntries(all: ExprOf<"intrinsic">): ExprOf<"arrayLit"> | null { + if (all.name !== "promise.all" || all.type.kind !== "promise") return null; + if (all.type.inner.kind !== "array") return null; + const arg = all.args[0]; + if (arg?.kind === "varRef") { + const deferred = this.deferredPromiseAllEntries.get(arg.localId); + if (deferred === undefined) return null; + this.deferredPromiseAllEntries.delete(arg.localId); + return deferred; + } + return arg?.kind === "arrayLit" && this.promiseAllInlineable(arg) ? arg : null; + } + + /** Whether every entry of `entries` is a call an await could run in place + * (inlineAwaitEligible) that yields its promise's inner type. */ + private promiseAllInlineable(entries: ExprOf<"arrayLit">): boolean { + if (entries.elems.length === 0 || (entries.spreads?.length ?? 0) > 0) return false; + if (entries.type.kind !== "array" || entries.type.elem.kind !== "promise") return false; + const inner = entries.type.elem.inner; + return entries.elems.every((el) => { + if (el.kind !== "call" || !this.inlineAwaitEligible(el.callee)) return false; + const callee = this.fnByName.get(el.callee)!; + return typeEquals(callee.returnType, inner) && !this.callLifetimes.borrowed.has(el.callee); + }); + } + + /** Whether `stmts[i]` declares a local holding an array literal that only + * `stmts[i + 1]` reads, as the operand of `await Promise.all(local)` that + * runs first in that statement and may run its entries in place. Nothing + * runs between the literal and the await, so the await evaluates the + * literal itself (inlinePromiseAllEntries). Debug builds keep the local. */ + deferredPromiseAllLocal(stmts: readonly IrStmt[], i: number): ExprOf<"arrayLit"> | null { + const decl = stmts[i]; + const next = stmts[i + 1]; + if (this.debug !== null || decl?.kind !== "varDecl" || decl.init?.kind !== "arrayLit") + return null; + if (this.currentCoro?.kind !== "async" || next === undefined) return null; + const operand = + next.kind === "varDecl" + ? next.init + : next.kind === "exprStmt" + ? next.expr + : next.kind === "assign" + ? next.value + : null; + if (operand?.kind !== "awaitExpr" || operand.adopt === true) return null; + const all = operand.value; + if (all.kind !== "intrinsic" || all.name !== "promise.all" || all.type.kind !== "promise") + return null; + if (all.type.inner.kind !== "array") return null; + const arg = all.args[0]; + if (arg?.kind !== "varRef" || arg.localId !== decl.localId) return null; + if (!this.promiseAllInlineable(decl.init)) return null; + if (this.currentLocals.get(decl.localId)?.boxed) return null; + // The declaration and this read are the local's only mentions. + return countLocalMentions(this.currentFunctionBody, decl.localId) === 2 ? decl.init : null; + } + + /** A stackless coroutine body's `await Promise.all([f(...), ...])` of + * never-suspending calls, with Promise.all's turns and without its + * promises: the entries run in place in order and their values fill the + * result array. Promise.all queues one reaction job per entry, back to + * back, and the last of them settles the result, whose await then takes a + * turn: here one turn stands for the entries' jobs and a second for the + * await. A throwing entry becomes the rejection the await re-throws from + * its turn; the remaining entries still run, and later throws are handled + * rejections, as Promise.all's subscriptions make them. */ + emitCoroInlinePromiseAll(entries: ExprOf<"arrayLit">, type: IrType): LlValue { + if (type.kind !== "array") + throw new InternalCompilerError("llvm emitter bug: inline Promise.all result"); + const B = this.B; + const elem = type.elem; + const acc = elemAccess(elem); + const values = B.tmp(); + // Up to three values fit in the array's own block (scr_arr_new_*_small). + const small = entries.elems.length <= 3; + B.line( + `${values} = ${arrNewCall(this.shapeHost, elem, small ? "0" : String(entries.elems.length), small)}`, + ); + const out = this.own({ name: values, type }); + this.declare(`declare ptr @scr_promise_new()`); + this.declare(`declare void @scr_promise_reject_pending(ptr)`); + this.declare(`declare void @scr_promise_mark_handled(ptr)`); + const promiseType = entries.type.kind === "array" ? entries.type.elem : type; + // Each entry's rejection, owned by the frame: unobserved until the + // entries have all run, as Promise.all subscribes only then, so an + // entry argument's synchronous throw leaves them unhandled. + const rejections: string[] = []; + for (const entry of entries.elems) { + if (entry.kind !== "call") + throw new InternalCompilerError("llvm emitter bug: inline Promise.all entry"); + const slot = B.tmp(); + B.entryAllocas.push(`${slot} = alloca ptr`); + B.line(`store ptr null, ptr ${slot}`); + this.ownSlot(slot, promiseType); + rejections.push(slot); + const value = emitCallExpr(this, entry, "inline"); + const thrown = B.tmp(); + for (const line of this.pendingTestLines(thrown)) B.line(line.trim()); + const clean = B.newLabel("pa.clean"); + const rejected = B.newLabel("pa.rejected"); + const next = B.newLabel("pa.next"); + B.condBr(thrown, rejected, clean); + B.startBlock(clean); + if (acc === "ref") this.toHeap(value); + this.arrPush(values, acc, value.name); + B.br(next); + B.startBlock(rejected); + const promise = B.tmp(); + B.line(`${promise} = call ptr @scr_promise_new()`); + B.line(`call void @scr_promise_reject_pending(ptr ${promise})`); + B.line(`store ptr ${promise}, ptr ${slot}`); + B.br(next); + B.startBlock(next); + } + // Promise.all's subscriptions handle every rejection, and the first + // (in entry order) rejects the result. + let first = "null"; + for (let k = rejections.length - 1; k >= 0; k--) { + const slot = rejections[k]!; + const rejection = B.tmp(); + const present = B.tmp(); + B.line(`${rejection} = load ptr, ptr ${slot}`); + B.line(`${present} = icmp ne ptr ${rejection}, null`); + const mark = B.newLabel("pa.handled"); + const after = B.newLabel("pa.unrejected"); + B.condBr(present, mark, after); + B.startBlock(mark); + B.line(`call void @scr_promise_mark_handled(ptr ${rejection})`); + B.br(after); + B.startBlock(after); + const chosen = B.tmp(); + B.line(`${chosen} = select i1 ${present}, ptr ${rejection}, ptr ${first}`); + first = chosen; + } + // The entries' reaction jobs. + this.emitCoroAwait(null); + const failed = B.tmp(); + B.line(`${failed} = icmp ne ptr ${first}, null`); + const fulfilled = B.newLabel("pa.fulfilled"); + const rejected = B.newLabel("pa.reject"); + const join = B.newLabel("pa.join"); + B.condBr(failed, rejected, fulfilled); + B.startBlock(fulfilled); + this.emitCoroAwait(null); + B.br(join); + B.startBlock(rejected); + this.declare(`declare void @scr_coro_await_void(ptr)`); + this.emitCoroAwait(first); + B.line(`call void @scr_coro_await_void(ptr ${first})`); + B.br(join); + B.startBlock(join); + // The rejections are released here, only where an entry threw, rather + // than by the frame, which releases each slot whether or not it holds a + // promise. + this.declare(`declare void @scr_promise_release(ptr)`); + for (const slot of rejections) { + const rejection = B.tmp(); + const present = B.tmp(); + B.line(`${rejection} = load ptr, ptr ${slot}`); + B.line(`${present} = icmp ne ptr ${rejection}, null`); + const release = B.newLabel("pa.release"); + const after = B.newLabel("pa.released"); + B.condBr(present, release, after); + B.startBlock(release); + B.line(`call void @scr_promise_release(ptr ${rejection})`); + B.br(after); + B.startBlock(after); + } + this.disownSlots(rejections.map((ptr) => ({ ptr }))); + this.emitPendingCheck(); + return out; + } emitAsyncAwaitCall(fn: IrFunction, argPackLines: string[] | null, adopt = false): string[] { return emitAsyncAwaitCall(this, fn, argPackLines, adopt); } @@ -4499,6 +4709,11 @@ export class LlEmitter { if (this.B.isTerminated()) return; if (cursor) cursor.index = i; this.stmtCursor = cursor; + const deferred = this.deferredPromiseAllLocal(stmts, i); + if (deferred !== null) { + this.deferredPromiseAllEntries.set((stmts[i] as { localId: string }).localId, deferred); + continue; + } this.emitStmt(stmts[i]!); } } finally { diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index 6da63977dc..4d73d09047 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -901,6 +901,12 @@ export function emitAsyncExpr( // the callee's adopting entry. Past finally regions, the return has // already produced the promise (adoptedOperand). const adopted = e.adopt === true ? host.takeAdoptedOperand() : null; + // A stackless coroutine body runs the entries of `Promise.all` of + // never-suspending calls in place and takes Promise.all's turns. + if (adopted === null && e.value.kind === "intrinsic" && host.currentCoro?.kind === "async") { + const entries = host.inlinePromiseAllEntries(e.value); + if (entries !== null) return host.emitCoroInlinePromiseAll(entries, e.type); + } // A stackless coroutine body runs a never-suspending callee in place // and takes the await's turn itself. if (adopted === null && e.value.kind === "call" && host.inlineAwaitEligible(e.value.callee)) { diff --git a/packages/compiler/test/await-call-emission.test.ts b/packages/compiler/test/await-call-emission.test.ts index b206b829b5..5d2b614c30 100644 --- a/packages/compiler/test/await-call-emission.test.ts +++ b/packages/compiler/test/await-call-emission.test.ts @@ -262,3 +262,48 @@ void main(); } } }); + +const promiseAllSource = ` +async function leaf(x: number): Promise { + return x + 1; +} +async function literal(x: number): Promise { + const all: number[] = await Promise.all([leaf(x), leaf(x + 1)]); + return all[0]! + all[1]!; +} +async function local(x: number): Promise { + const ps = [leaf(x), leaf(x + 1), leaf(x + 2)]; + const all = await Promise.all(ps); + return all.length; +} +async function reused(x: number): Promise { + const ps = [leaf(x)]; + const all = await Promise.all(ps); + return all.length + ps.length; +} +async function main(): Promise { + console.log((await literal(1)) + (await local(2)) + (await reused(3))); +} +void main(); +`; + +test("a stackless body runs Promise.all of never-suspending calls in place", async () => { + const llvm = await emit(promiseAllSource); + for (const [symbol, calls] of [ + ["sc_f_literal", 2], + ["sc_f_local", 3], + ] as const) { + const body = definition(llvm, symbol); + // The entries run in place and fill the result array; one turn stands + // for Promise.all's reaction jobs and one for the await. + expect(body.match(/call double @sc_ai_leaf\(double/g)?.length, symbol).toBe(calls); + expect(body.match(/call zeroext i1 @scr_coro_hop_prepare\(ptr/g)?.length, symbol).toBe(2); + expect(body, symbol).toContain("call void @scr_promise_mark_handled(ptr"); + expect(body, symbol).not.toContain("@scr_promise_all("); + expect(body, symbol).not.toContain("@sc_as_leaf("); + } + // A local the body reads again keeps its promises. + const reused = definition(llvm, "sc_f_reused"); + expect(reused).toContain("call ptr @sc_as_leaf(double"); + expect(reused).toContain("@scr_promise_all("); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7100-promise-all-inline-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7100-promise-all-inline-order.ts.json new file mode 100644 index 0000000000..1dd147700b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7100-promise-all-inline-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7100-promise-all-inline-order.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7101-promise-all-inline-sync-throw.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7101-promise-all-inline-sync-throw.ts.json new file mode 100644 index 0000000000..9ee7561669 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7101-promise-all-inline-sync-throw.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7101-promise-all-inline-sync-throw.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7102-promise-all-inline-kinds.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7102-promise-all-inline-kinds.ts.json new file mode 100644 index 0000000000..572b900139 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7102-promise-all-inline-kinds.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7102-promise-all-inline-kinds.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7100-promise-all-inline-order.ts b/tests/corpus/7100-promise-all-inline-order.ts new file mode 100644 index 0000000000..b67eca0143 --- /dev/null +++ b/tests/corpus/7100-promise-all-inline-order.ts @@ -0,0 +1,86 @@ +// Ordering of `await Promise.all([...])` of never-suspending async calls +// against concurrent microtasks. +const log: string[] = []; + +async function value(n: number): Promise { + log.push(`value ${n}`); + return n * 10; +} +async function fail(tag: string): Promise { + log.push(`fail ${tag}`); + throw new Error(`boom ${tag}`); +} +async function label(s: string): Promise { + log.push(`label ${s}`); + return s.toUpperCase(); +} + +function ticker(name: string, turns: number): Promise { + let p = Promise.resolve(); + for (let i = 0; i < turns; i++) p = p.then(() => void log.push(`${name} tick ${i}`)); + return p; +} + +async function literal(): Promise { + const r: number[] = await Promise.all([value(1), value(2), value(3)]); + log.push(`literal ${r.join(",")}`); +} +async function local(): Promise { + const ps = [value(4), value(5)]; + const r = await Promise.all(ps); + log.push(`local ${r.join(",")}`); +} +async function firstRejects(): Promise { + try { + await Promise.all([fail("a"), value(6), fail("b")]); + log.push("firstRejects resolved"); + } catch (e) { + log.push(`firstRejects caught ${(e as Error).message}`); + } +} +async function middleRejects(): Promise { + const ps = [value(7), fail("c"), value(8)]; + try { + const r = await Promise.all(ps); + log.push(`middleRejects resolved ${r.length}`); + } catch (e) { + log.push(`middleRejects caught ${(e as Error).message}`); + } +} +async function strings(): Promise { + const r: string[] = await Promise.all([label("x"), label("y")]); + log.push(`strings ${r.join("+")}`); +} +async function single(): Promise { + const all: number[] = await Promise.all([value(9)]); + const only = all[0]; + log.push(`single ${only}`); +} +async function reused(): Promise { + const ps = [value(11), value(12)]; + const r = await Promise.all(ps); + log.push(`reused ${r.join(",")} ${ps.length}`); +} +async function afterAssign(): Promise { + let r: number[] = []; + const ps = [value(13)]; + r = await Promise.all(ps); + log.push(`afterAssign ${r.join(",")}`); +} + +async function main(): Promise { + await Promise.all([ + ticker("t", 8), + literal(), + ticker("u", 4), + local(), + firstRejects(), + middleRejects(), + strings(), + single(), + reused(), + afterAssign(), + ]); + for (const line of log) console.log(line); +} +void main(); diff --git a/tests/corpus/7101-promise-all-inline-sync-throw.ts b/tests/corpus/7101-promise-all-inline-sync-throw.ts new file mode 100644 index 0000000000..43e3b1167f --- /dev/null +++ b/tests/corpus/7101-promise-all-inline-sync-throw.ts @@ -0,0 +1,21 @@ +// An entry argument that throws synchronously leaves earlier rejections +// unhandled, since Promise.all never runs. +async function fail(tag: string): Promise { + throw new Error(`boom ${tag}`); +} +async function value(n: number): Promise { + return n; +} +function boom(): number { + throw new Error("argument"); +} +async function run(): Promise { + try { + const r: number[] = await Promise.all([fail("first"), value(boom())]); + console.log("resolved", r.length); + } catch (e) { + console.log("caught", (e as Error).message); + } +} +process.on("unhandledRejection", (reason) => console.log("unhandled", (reason as Error).message)); +void run().then(() => console.log("done")); diff --git a/tests/corpus/7102-promise-all-inline-kinds.ts b/tests/corpus/7102-promise-all-inline-kinds.ts new file mode 100644 index 0000000000..6036a4d46c --- /dev/null +++ b/tests/corpus/7102-promise-all-inline-kinds.ts @@ -0,0 +1,69 @@ +// `await Promise.all([...])` of never-suspending async calls with number, +// boolean, string and record results, inside loops and larger expressions, +// and an uncaught rejection. +interface Point { + x: number; + y: number; +} +const log: string[] = []; +async function flag(n: number): Promise { + log.push(`flag ${n}`); + return n % 2 === 0; +} +async function point(n: number): Promise { + log.push(`point ${n}`); + return { x: n, y: n * n }; +} +async function count(n: number): Promise { + if (n < 0) throw new RangeError(`negative ${n}`); + return n + 1; +} +function ticker(name: string, turns: number): Promise { + let p = Promise.resolve(); + for (let i = 0; i < turns; i++) p = p.then(() => void log.push(`${name} tick ${i}`)); + return p; +} +async function flags(): Promise { + const r: boolean[] = await Promise.all([flag(1), flag(2)]); + log.push(`flags ${r.join(",")}`); +} +async function points(): Promise { + const ps = [point(1), point(2), point(3)]; + const r = await Promise.all(ps); + let sum = 0; + for (const p of r) sum += p.x + p.y; + log.push(`points ${sum}`); +} +async function loop(): Promise { + let total = 0; + for (let i = 0; i < 3; i++) { + const ps = [count(i), count(i * 2)]; + const r = await Promise.all(ps); + total += r[0]! + r[1]!; + log.push(`loop ${i} ${total}`); + } +} +function sumOf(xs: number[]): number { + let s = 0; + for (const x of xs) s += x; + return s; +} +async function nested(): Promise { + const total = 100 + sumOf(await Promise.all([count(4), count(5)])); + log.push(`nested ${total}`); +} +async function uncaught(): Promise { + const ps = [count(1), count(-2), count(-3)]; + const r = await Promise.all(ps); + log.push(`uncaught resolved ${r.length}`); +} +process.on("unhandledRejection", (reason) => { + const e = reason as Error; + console.log(`unhandled ${e.name} ${e.message}`); +}); +async function main(): Promise { + void uncaught(); + await Promise.all([ticker("t", 6), flags(), points(), loop(), nested()]); + for (const line of log) console.log(line); +} +void main(); diff --git a/tests/harness/corpus-costs/7100-promise-all-inline-order.ts.json b/tests/harness/corpus-costs/7100-promise-all-inline-order.ts.json new file mode 100644 index 0000000000..054b3c2b49 --- /dev/null +++ b/tests/harness/corpus-costs/7100-promise-all-inline-order.ts.json @@ -0,0 +1 @@ +{ "plain": 3900, "san": 5000 } diff --git a/tests/harness/corpus-costs/7101-promise-all-inline-sync-throw.ts.json b/tests/harness/corpus-costs/7101-promise-all-inline-sync-throw.ts.json new file mode 100644 index 0000000000..71bf780f8d --- /dev/null +++ b/tests/harness/corpus-costs/7101-promise-all-inline-sync-throw.ts.json @@ -0,0 +1 @@ +{ "plain": 2700, "san": 3500 } diff --git a/tests/harness/corpus-costs/7102-promise-all-inline-kinds.ts.json b/tests/harness/corpus-costs/7102-promise-all-inline-kinds.ts.json new file mode 100644 index 0000000000..42d31979d3 --- /dev/null +++ b/tests/harness/corpus-costs/7102-promise-all-inline-kinds.ts.json @@ -0,0 +1 @@ +{ "plain": 3200, "san": 4000 } From 956bdf4ec0045ee6aca671dc720075409bbd7f3b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 23:49:13 -0500 Subject: [PATCH 055/395] Bound while-loop counters that nested scans advance A while loop's counter gets an integer range when the loop only adds positive literals to it and its condition bounds it. A counter written inside a nested loop was refused, which is every hand-written scanner: `while (pos < end) { ... while (pos < end && isDigit(...)) pos++; ... }`. A nested while loop may now advance the counter when its own condition bounds it the same way and does not write it. Its passes start at most at that bound, so it leaves the counter at its entry value or at the highest value one of its passes reaches, and the outer loop's range follows from there. Other nested loops, and nested loops that do not bound the counter, still refuse it. --- .../compiler/src/ir/integer-loop-facts.ts | 74 ++++++++++++++++--- .../compiler/src/ir/integer-ranges.test.ts | 68 +++++++++++++++++ 2 files changed, 130 insertions(+), 12 deletions(-) diff --git a/packages/compiler/src/ir/integer-loop-facts.ts b/packages/compiler/src/ir/integer-loop-facts.ts index 23982aa078..b075ef29a3 100644 --- a/packages/compiler/src/ir/integer-loop-facts.ts +++ b/packages/compiler/src/ir/integer-loop-facts.ts @@ -246,10 +246,15 @@ export const STRING_MAX_UNITS = 0x7fffffff; /** Bound counters of a while loop by its condition. A counter qualifies when * the loop (condition and body) changes it only by increments of positive - * integer literals outside nested loops, switch and exception regions, and a - * conjunct of the condition compares it below a literal, a binding the loop - * never writes, or the length of a string binding the loop never writes - * (`while (x * x + y * y <= 4 && it < MAX) it++`, `while (pos < text.length)`). + * integer literals outside switch and exception regions and loops other + * than nested while loops, and a conjunct of the condition compares it below + * a literal, a binding the loop never writes, or the length of a string + * binding the loop never writes (`while (x * x + y * y <= 4 && it < MAX) + * it++`, `while (pos < text.length)`). A nested while loop may advance the + * counter only when its own condition bounds it the same way and does not + * write it (a scanner's `while (pos < end && isDigit(...)) pos++`): its + * passes start at most at that bound, so it leaves the counter at its entry + * value or at the highest value one of its passes reaches. * The body runs only after that conjunct held, and the condition never * writes the counter, so every pass starts at most at the conjunct's bound. * The pass then climbs by the increments on its path; an increment inside an @@ -270,19 +275,29 @@ export function guardedLoopCounterFacts( if (opaque || delta === null || delta <= 0 || delta > 2147483647) refused.add(id); else budget.set(id, (budget.get(id) ?? 0) + delta); }; + // Nested while loops outside opaque regions, checked against their own + // conditions once the stable bounds are known. + const nested: (IrStmt & { kind: "while" })[] = []; const expr = (e: IrExpr): boolean => { if (e.kind === "assignExpr") write(e.localId, increment(e.localId, e.value)); if (e.kind === "incDec") write(e.localId, e.op === "+" ? 1 : null); return everyExprChild(e, expr, stmt); }; + const condition = new Set(); const stmt = (s: IrStmt): boolean => { + const bounded = s.kind === "while" && opaque === 0 && !s.labels?.length; const region = - s.kind === "for" || - s.kind === "while" || - s.kind === "doWhile" || - s.kind === "forOf" || - s.kind === "tryCatch" || - s.kind === "switch"; + !bounded && + (s.kind === "for" || + s.kind === "while" || + s.kind === "doWhile" || + s.kind === "forOf" || + s.kind === "tryCatch" || + s.kind === "switch"); + if (bounded) { + nested.push(s); + conditionWrites(s.cond); + } if (region) opaque++; if (s.kind === "assign") write(s.localId, increment(s.localId, s.value)); if (s.kind === "varDecl" || s.kind === "forOf") write(s.localId, null); @@ -293,7 +308,6 @@ export function guardedLoopCounterFacts( }; // The condition is evaluated before every pass; a write there would // separate the compared value from the one the body sees. - const condition = new Set(); const conditionWrites = (e: IrExpr): boolean => { if (e.kind === "assignExpr" || e.kind === "incDec") condition.add(e.localId); return everyExprChild(e, conditionWrites, (s) => { @@ -304,8 +318,9 @@ export function guardedLoopCounterFacts( }); }; conditionWrites(loop.cond); - for (const id of condition) refused.add(id); loop.body.every(stmt); + // Nested loops' conditions are collected during the walk. + for (const id of condition) refused.add(id); const stable = (id: string): boolean => { const local = locals.get(id); return local !== undefined && !local.boxed && !local.tdz && !budget.has(id) && !refused.has(id); @@ -359,6 +374,11 @@ export function guardedLoopCounterFacts( } return ceiling; }; + // A nested while loop advances only the counters its condition bounds. + for (const inner of nested) { + const caps = ceilings(inner.cond); + for (const id of writtenLocals(inner.body)) if (!caps.has(id)) refused.add(id); + } const ceiling = ceilings(loop.cond); for (const id of budget.keys()) { const local = locals.get(id), @@ -416,6 +436,21 @@ function highestCount( count = Math.max(then, else_); } else if (s.kind === "block" && !s.labels?.length) { count = walk(s.body, count); + } else if ( + s.kind === "while" && + !s.labels?.length && + increments(s.cond, true) === 0 && + ceilings(s.cond).has(id) + ) { + // Every pass starts at most at the bound; the loop ends at its entry + // value or where a pass left the counter. + const cap = ceilings(s.cond).get(id)!; + const outer = highest; + highest = cap; + walk(s.body, cap); + const inner = highest; + highest = Math.max(outer, inner); + count = Math.max(count, inner); } else { count += increments(s, false); highest = Math.max(highest, count); @@ -435,6 +470,21 @@ function highestCount( return highest; } +/** The bindings `body` assigns or increments anywhere. */ +function writtenLocals(body: readonly IrStmt[]): Set { + const ids = new Set(); + const expr = (e: IrExpr): boolean => { + if (e.kind === "assignExpr" || e.kind === "incDec") ids.add(e.localId); + return everyExprChild(e, expr, stmt); + }; + const stmt = (s: IrStmt): boolean => { + if (s.kind === "assign") ids.add(s.localId); + return everyStmtChild(s, expr, stmt); + }; + body.every(stmt); + return ids; +} + /** Recognize only exits whose target cannot be inside this statement list. * Labeled blocks, loops, switch and exception regions retain the join. */ export function integerPathFallsThrough(body: readonly IrStmt[]): boolean { diff --git a/packages/compiler/src/ir/integer-ranges.test.ts b/packages/compiler/src/ir/integer-ranges.test.ts index 32caea6721..b495eb814f 100644 --- a/packages/compiler/src/ir/integer-ranges.test.ts +++ b/packages/compiler/src/ir/integer-ranges.test.ts @@ -797,6 +797,74 @@ test("a while condition on a string's length bounds a counter, and guarded incre expect(run((inside) => [observe(inside), bump, rebound]).after).toBeNull(); }); +test("a nested while loop may advance a counter its own condition bounds", () => { + // while (pos < text.length) { observe(pos); while (pos < text.length && unknown === 1) pos++; pos += 2; } + const text: IrExpr = { loc, kind: "varRef", localId: "text", type: STRING }; + const length: IrExpr = { + loc, + kind: "strIntrinsic", + method: "length", + receiver: text, + args: [], + type: F64, + }; + const bump: IrStmt = { + loc, + kind: "exprStmt", + expr: { loc, kind: "incDec", op: "+", prefix: false, localId: "pos", type: F64 }, + }; + const skip: IrStmt = { loc, kind: "assign", localId: "pos", value: bin("+", ref("pos"), num(2)) }; + const and = (left: IrExpr, right: IrExpr): IrExpr => ({ + loc, + kind: "logical", + op: "&&", + left, + right, + type: BOOL, + }); + const scan = (cond: IrExpr): IrStmt => ({ loc, kind: "while", cond, body: [bump] }); + const run = (inner: IrStmt) => { + const inside = ref("pos"), + after = ref("pos"); + const f = fn([ + { loc, kind: "assign", localId: "pos", value: num(0) }, + { + loc, + kind: "while", + cond: condition("<", ref("pos"), length), + body: [observe(inside), inner, skip], + }, + observe(after), + ]); + f.locals.push( + { id: "pos", name: "pos", type: F64, mutable: true }, + { id: "text", name: "text", type: STRING, mutable: false }, + ); + const ranges = analyzeIntegerRanges(f); + return { inside: ranges.get(inside), after: ranges.get(after) }; + }; + // Each inner pass starts below the length, so the scan stops at it at the + // latest; the skip then adds 2. + const bounded = scan( + and(condition("<", ref("pos"), length), condition("===", ref("unknown"), num(1))), + ); + expect(run(bounded)).toEqual({ + inside: { min: 0, max: 2147483646 }, + after: { min: 0, max: 2147483649 }, + }); + // A nested loop that does not bound the counter, or writes it in its + // condition, leaves it unbounded. + expect(run(scan(condition("===", ref("unknown"), num(1)))).after).toBeNull(); + const writing = scan( + condition( + "<", + { loc, kind: "incDec", op: "+", prefix: true, localId: "pos", type: F64 }, + length, + ), + ); + expect(run(writing).after).toBeNull(); +}); + test("an array length bounds while and for counters even when the body resizes the array", () => { // let i = 0; while (i < items.length) { observe(i); items.push(i); i++; } observe(i); const ARR = { kind: "array", elem: F64 } as const; From 63001cbc136b3f77653750755f20796022507702 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 23:55:37 -0500 Subject: [PATCH 056/395] Await a suspending stackless callee without its promise --- packages/cli/test/native-link-info.test.ts | 4 +- .../src/backend/llvm/emit-function.ts | 15 ++ packages/compiler/src/backend/llvm/emitter.ts | 66 +++++++++ .../compiler/src/backend/llvm/expr-async.ts | 16 +++ .../compiler/src/backend/llvm/expr-calls.ts | 11 +- packages/compiler/src/backend/mangle.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 +- .../compiler/test/await-call-emission.test.ts | 52 ++++++- .../test/native-codegen-integration.test.ts | 4 +- .../corpus/7104-direct-await-order.ts.json | 6 + .../corpus/7105-direct-await-context.ts.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_async.c | 133 +++++++++++++++++- packages/runtime/src/scr_async.h | 9 ++ packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- tests/corpus/7104-direct-await-order.ts | 96 +++++++++++++ tests/corpus/7105-direct-await-context.ts | 34 +++++ .../7104-direct-await-order.ts.json | 1 + .../7105-direct-await-context.ts.json | 1 + 23 files changed, 452 insertions(+), 23 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7104-direct-await-order.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7105-direct-await-context.ts.json create mode 100644 tests/corpus/7104-direct-await-order.ts create mode 100644 tests/corpus/7105-direct-await-context.ts create mode 100644 tests/harness/corpus-costs/7104-direct-await-order.ts.json create mode 100644 tests/harness/corpus-costs/7105-direct-await-context.ts.json diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 57c60b1bc8..f5ff2642e3 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: 23, marker: "scr_runtime_abi_v23" }, + runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, 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_v23"); + expect(error?.stderr).toContain("scr_runtime_abi_v24"); }); }); diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 7bbd3d4538..8c6264e6ca 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -35,6 +35,7 @@ import { mangleArgPack, mangleAsyncAdoptCall, mangleAsyncAwaitCall, + mangleAsyncDirectSpawn, mangleAsyncSpawn, mangleBorrowedFunction, mangleClassStruct, @@ -910,6 +911,20 @@ export function emitAsyncScaffolding(emitter: LlEmitter): string[] { for (const line of emitter.emitAsyncAwaitCall(fn, argPackLines, true)) out.push(line); } const stackless = emitter.stacklessAsync.has(fn.name); + if (stackless && emitter.directAwaitCallees.has(fn.name)) { + // A coroutine's direct await: the same prefix, with the promise kept + // inside the returned fiber record. + emitter.declare(`declare ptr @scr_async_spawn_direct(ptr, ptr)`); + out.push( + `define internal ptr @${mangleAsyncDirectSpawn(fn.name)}(${spawnParams.join(", ")}) ${FN_ATTRS} { ; direct spawn ${fn.name}`, + `entry:`, + ...argPackLines, + ` %f = call ptr @scr_async_spawn_direct(ptr @${mangleTrampoline(fn.name)}, ptr %ap)`, + ` ret ptr %f`, + `}`, + ``, + ); + } const spawn = stackless ? "scr_async_spawn_stackless" : "scr_async_spawn"; emitter.declare(`declare ptr @scr_fiber_promise(ptr)`); emitter.declare(`declare ptr @${spawn}(ptr, ptr)`); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index b306674fc6..eeb89d953c 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -889,6 +889,9 @@ export class LlEmitter { /** Fiberless async functions a stackless coroutine body awaits directly; * each gets an inline await entry (emitCoroInlineAwait). */ readonly inlineAwaitCallees = new Set(); + /** Stackless async functions some coroutine body awaits directly: each + * gets a direct spawn entry (emitCoroDirectAwait). */ + readonly directAwaitCallees = new Set(); /** The async functions whose bodies never suspend and that a direct * await may enter in place (selectStacklessAsync). */ private inlineAwaitable = new Set(); @@ -1913,6 +1916,69 @@ export class LlEmitter { return fn.captures === undefined; } + /** Whether a stackless coroutine body's `await f(...)` may await f's + * fiber record instead of its promise (emitCoroDirectAwait): f is a + * stackless async function whose direct call an await-call entry could + * take. The await consumes the promise, so nothing else can observe it. + * Worker programs keep the promise for their stop checkpoint. */ + directAwaitEligible(fnName: string): boolean { + if (this.wasi || this.mod.workers === true || this.currentCoro?.kind !== "async") return false; + const fn = this.fnByName.get(fnName); + if (fn === undefined || fn.async !== true || fn.generator !== undefined) return false; + if (!this.stacklessAsync.has(fnName) || fn.captures !== undefined) return false; + if (fn.returnType.kind === "dyn") return false; + return fn.asyncCacheGlobal === undefined && fn.asyncCycleCacheGlobal === undefined; + } + + /** A stackless coroutine body's `await f(...)` of a stackless f, without + * f's promise object: f's prefix runs as the spawn of a fiber record that + * keeps the promise inside. A body still running wakes this coroutine + * when it finishes, where its promise's settlement would wake its one + * waiter; a finished one takes the settled await's turn. The result (or + * the rejection, re-thrown) comes out of the record, which goes. */ + emitCoroDirectAwait(call: ExprOf<"call">, type: IrType): LlValue { + const coro = this.currentCoro; + if (coro === null) + throw new InternalCompilerError("llvm emitter bug: direct await outside a coroutine"); + const B = this.B; + const record = emitCallExpr(this, call, "direct").name; + this.declare(`declare zeroext i1 @scr_coro_direct_prepare(ptr, ptr)`); + const suspend = B.tmp(); + B.line( + `${suspend} = call zeroext i1 @scr_coro_direct_prepare(ptr ${coro.self}, ptr ${record})`, + ); + const park = B.newLabel("await.suspend"); + const ready = B.newLabel("await.ready"); + B.condBr(suspend, park, ready); + B.startBlock(park); + this.emitCoroSuspendPrepared(); + B.br(ready); + B.startBlock(ready); + if (type.kind === "void") { + this.declare(`declare void @scr_coro_direct_void(ptr)`); + B.line(`call void @scr_coro_direct_void(ptr ${record})`); + this.emitPendingCheck(); + return { name: "", type }; + } + const t = B.tmp(); + if (type.kind === "f64" || type.kind === "date") { + this.declare(`declare double @scr_coro_direct_f64(ptr)`); + B.line(`${t} = call double @scr_coro_direct_f64(ptr ${record})`); + } else if (type.kind === "bool") { + this.declare(`declare zeroext i1 @scr_coro_direct_bool(ptr)`); + B.line(`${t} = call zeroext i1 @scr_coro_direct_bool(ptr ${record})`); + } else if (type.kind === "string") { + this.declare(`declare ptr @scr_coro_direct_str(ptr)`); + B.line(`${t} = call ptr @scr_coro_direct_str(ptr ${record})`); + } else { + this.declare(`declare ptr @scr_coro_direct_ref(ptr)`); + B.line(`${t} = call ptr @scr_coro_direct_ref(ptr ${record})`); + } + const out = this.own({ name: this.adoptHeap(t, type), type }); + this.emitPendingCheck(); + return out; + } + /** The inline await entry of a fiberless async function (`sc_ai_*`): its * body on the caller's stack (between scr_async_inline_enter/exit when * fiberlessFrames), as in its spawn wrapper, returning the result and diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index 4d73d09047..b5d765b16a 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -918,6 +918,22 @@ export function emitAsyncExpr( return host.emitCoroInlineAwait(e.value, e.adopt === true); } } + // A stackless coroutine body awaits a stackless callee's fiber record + // instead of its promise. + if ( + adopted === null && + e.adopt !== true && + e.value.kind === "call" && + host.directAwaitEligible(e.value.callee) + ) { + const callee = host.fnByName.get(e.value.callee)!; + if ( + typeEquals(callee.returnType, e.type) && + !host.callLifetimes.borrowed.has(e.value.callee) + ) { + return host.emitCoroDirectAwait(e.value, e.type); + } + } if (adopted === null && e.value.kind === "call" && host.awaitCallEligible(e.value.callee)) { const callee = host.fnByName.get(e.value.callee)!; if ( diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index 81c44d5a6b..f0aa3b276d 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -31,6 +31,7 @@ import { mangleFunction, mangleWrapper, mangleVtStruct, + mangleAsyncDirectSpawn, } from "../mangle.js"; import { classEnvironmentIndex, @@ -74,7 +75,7 @@ export function emitCallExpr( | "instanceOf" | "virtualCall" >, - awaitEntry: false | "await" | "adopt" | "inline" = false, + awaitEntry: false | "await" | "adopt" | "inline" | "direct" = false, /** A projection consumes a scalar union result (Int32Abi.union): answer * a private stack box instead of a heap box. */ stackResult = false, @@ -184,6 +185,14 @@ export function emitCallExpr( : `${host.llType(callee.params[i]!.type)} ${a.name}`, ) .join(", "); + if (awaitEntry === "direct") { + // A stackless coroutine's await of a stackless callee: the spawn + // returns the callee's fiber record (emitCoroDirectAwait). + host.directAwaitCallees.add(e.callee); + const record = B.tmp(); + B.line(`${record} = call ptr @${mangleAsyncDirectSpawn(e.callee)}(${argList})`); + return { name: record, type: callee.returnType }; + } if (awaitEntry === "inline") { // A stackless coroutine's await of a fiberless callee: the entry // runs the body in place and leaves a throw pending; the enclosing diff --git a/packages/compiler/src/backend/mangle.ts b/packages/compiler/src/backend/mangle.ts index da6cbe0f55..14736ead72 100644 --- a/packages/compiler/src/backend/mangle.ts +++ b/packages/compiler/src/backend/mangle.ts @@ -195,6 +195,11 @@ export function mangleAsyncAdoptCall(fnName: string): string { export function mangleAsyncInlineAwaitCall(fnName: string): string { return `sc_ai_${sanitize(fnName)}`; } +/** A stackless coroutine body's `await f(...)` of a stackless async f: the + * spawn of f's fiber record, which keeps its promise inside (direct await). */ +export function mangleAsyncDirectSpawn(fnName: string): string { + return `sc_dw_${sanitize(fnName)}`; +} export function mangleTrampoline(fnName: string): string { return `sc_tr_${sanitize(fnName)}`; } diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 67be865715..5eb24482bb 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 = 23 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v23" as const; +export const RUNTIME_ABI_VERSION = 24 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v24" 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 19519e1f22..fc7ed9efa0 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: 23, marker: "scr_runtime_abi_v23" }, + runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, 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 ee2301172f..5ffb4797de 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -124,7 +124,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" }, + runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/test/await-call-emission.test.ts b/packages/compiler/test/await-call-emission.test.ts index 5d2b614c30..96c039155c 100644 --- a/packages/compiler/test/await-call-emission.test.ts +++ b/packages/compiler/test/await-call-emission.test.ts @@ -183,10 +183,16 @@ test("bodies that suspend only at their own awaits compile to stackless coroutin const middle = definition(llvm, "sc_f_middle"); expect(middle).toMatch(/^define internal double @sc_f_middle\(double [^)]*\) #1 /); // Each await prepares its continuation and suspends the coroutine, then - // extracts the settled result; nothing parks the stack it runs on. - expect(middle).toContain("call zeroext i1 @scr_coro_await_prepare(ptr"); - expect(middle).toContain("call double @scr_coro_await_f64(ptr"); - expect(middle).toContain("call ptr @sc_as_slow(double"); + // extracts the settled result; nothing parks the stack it runs on. A + // stackless callee is awaited through its fiber record, without its + // promise (emitCoroDirectAwait). + expect(middle).toContain("call ptr @sc_dw_slow(double"); + expect(middle).toContain("call zeroext i1 @scr_coro_direct_prepare(ptr"); + expect(middle).toContain("call double @scr_coro_direct_f64(ptr"); + expect(middle).not.toContain("@sc_as_slow("); + expect(definition(llvm, "sc_dw_slow")).toContain( + "call ptr @scr_async_spawn_direct(ptr @sc_tr_slow", + ); // A callee that never suspends runs in place; the coroutine takes the // await's turn itself and awaits a rejected promise only on a throw. expect(middle).toContain("call double @sc_ai_leaf(double"); @@ -307,3 +313,41 @@ test("a stackless body runs Promise.all of never-suspending calls in place", asy expect(reused).toContain("call ptr @sc_as_leaf(double"); expect(reused).toContain("@scr_promise_all("); }); + +const directSource = ` +async function slow(x: number): Promise { + await null; + return x + 1; +} +async function direct(x: number): Promise { + return (await slow(x)) * 2; +} +async function adopting(x: number): Promise { + return slow(x); +} +async function kept(x: number): Promise { + const pending = slow(x); + await null; + return await pending; +} +async function main(): Promise { + console.log((await direct(1)) + (await adopting(2)) + (await kept(3))); +} +void main(); +`; + +test("a stackless body awaits a stackless callee's fiber record instead of its promise", async () => { + const llvm = await emit(directSource); + const direct = definition(llvm, "sc_f_direct"); + expect(direct).toContain("call ptr @sc_dw_slow(double"); + expect(direct).toContain("call zeroext i1 @scr_coro_direct_prepare(ptr"); + expect(direct).toContain("call double @scr_coro_direct_f64(ptr"); + expect(direct).not.toContain("@scr_coro_await_prepare("); + // A returned call adopts the callee's promise, and a promise held in a + // local is a value: both keep the promise. + for (const symbol of ["sc_f_adopting", "sc_f_kept"]) { + const body = definition(llvm, symbol); + expect(body, symbol).toContain("call ptr @sc_as_slow(double"); + expect(body, symbol).not.toContain("@sc_dw_slow("); + } +}); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index c0f2cfe845..d8f158ebcb 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_v23"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v24"); 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_v23"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v24"); }); 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/7104-direct-await-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7104-direct-await-order.ts.json new file mode 100644 index 0000000000..238ee8fb12 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7104-direct-await-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7104-direct-await-order.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7105-direct-await-context.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7105-direct-await-context.ts.json new file mode 100644 index 0000000000..2ed3b6aec7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7105-direct-await-context.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7105-direct-await-context.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 856de4b86c..85c5ffb000 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -337,7 +337,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" }, + runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index fa84e9c4d2..b242b78aa5 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -434,7 +434,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" }, + runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 12cf81f774..db1e54295b 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -616,6 +616,13 @@ struct ScrFiber { struct ScrFiber *awaiter; /* This fiber was parked by such a child; set before the switch away. */ bool await_parked; + /* A stackless body spawned for a direct await (scr_async_spawn_direct): + * its promise lives in the same block (ScrDirectFiber) and nothing else + * sees it. Once the awaiting coroutine suspends it is direct_awaiter, the + * finished body wakes it in place of the promise's settlement, and the + * awaiter destroys the record when it takes the result. */ + bool direct; + struct ScrFiber *direct_awaiter; #ifdef SCR_ASAN_FIBERS void *fake_stack; /* Live stackless async fibers (scr_async_spawn_stackless) form a list @@ -2138,11 +2145,31 @@ static void scr_prefix_on_fresh_stack(ScrFiber *f, void (*entry)(ScrFiber *, voi } #endif -ScrPromise *scr_async_spawn_stackless(void (*entry)(ScrFiber *, void *), void *argpack) { - ScrFiber *f = scr_mem_calloc(sizeof *f); - if (!f) scr_oom(); +/* A directly awaited body's fiber record and its promise, one block. */ +typedef struct { + ScrFiber fiber; + ScrPromise promise; +} ScrDirectFiber; + +/* Creates a stackless body's fiber and runs its eager prefix, which + * returns at the first suspension or the end. A direct fiber's promise is + * embedded and immortal (retains and releases skip it). */ +static ScrFiber *scr_spawn_stackless_fiber(void (*entry)(ScrFiber *, void *), void *argpack, + bool direct) { + ScrFiber *f; + if (direct) { + ScrDirectFiber *block = scr_mem_calloc(sizeof *block); + if (!block) scr_oom(); + f = &block->fiber; + f->promise = &block->promise; + f->promise->rc = SIZE_MAX; + f->direct = true; + } else { + f = scr_mem_calloc(sizeof *f); + if (!f) scr_oom(); + f->promise = scr_promise_new(); + } scr_fiber_track(f); - f->promise = scr_promise_new(); f->stackless = true; f->als = scr_als_ctx_retain(*SCR_ALS_SLOT()); /* The lifetime region the eager prefix runs in, which every later step @@ -2171,6 +2198,11 @@ ScrPromise *scr_async_spawn_stackless(void (*entry)(ScrFiber *, void *), void *a scr_current = spawner; scr_exc_swap_cell(spawner ? &spawner->exc : NULL); scr_als_active = spawner ? &spawner->als : &scr_als_main_slot; + return f; +} + +ScrPromise *scr_async_spawn_stackless(void (*entry)(ScrFiber *, void *), void *argpack) { + ScrFiber *f = scr_spawn_stackless_fiber(entry, argpack, false); ScrPromise *result = scr_promise_retain(f->promise); if (f->done) { scr_promise_release(f->promise); @@ -2179,6 +2211,14 @@ ScrPromise *scr_async_spawn_stackless(void (*entry)(ScrFiber *, void *), void *a } return result; } + +/* A stackless body that a stackless coroutine awaits right away (the + * compiler's direct await): its promise never escapes, so it is embedded + * in the fiber record, which the awaiter takes the result from + * (scr_coro_direct_*) and then destroys. */ +ScrFiber *scr_async_spawn_direct(void (*entry)(ScrFiber *, void *), void *argpack) { + return scr_spawn_stackless_fiber(entry, argpack, true); +} #endif static void scr_await_park(ScrPromise *p); @@ -2447,7 +2487,28 @@ bool scr_coro_await_prepare(ScrFiber *self, ScrPromise *p) { bool scr_coro_hop_prepare(ScrFiber *self) { return scr_coro_hop_needed(self); } -void scr_coro_async_finish(ScrFiber *self) { scr_fiber_finish(self); } +/* A directly awaited body's end: its awaiter is the promise's only waiter, + * and it observes a rejection when it takes the result, so the body wakes + * it where the settlement would, without the unhandled-rejection ledger. */ +static void scr_direct_finish(ScrFiber *self) { + ScrPromise *p = self->promise; + bool stopped = false; +#ifdef SCR_WORKERS + /* A stop request rejects like the fiber epilogue does (scr_fiber_finish). */ + stopped = scr_context_checkpoint(); +#endif + if (stopped || scr_exc_pending()) scr_promise_reject_from_cell(p, &self->exc); + if (stopped) p->rejection_observed = true; + if (p->state == SCR_PROM_PENDING) + scr_trap("scriptc: internal error: directly awaited body finished unsettled\n"); + self->done = true; + if (self->direct_awaiter != NULL) scr_promise_wake_one(self->direct_awaiter); +} + +void scr_coro_async_finish(ScrFiber *self) { + if (self->direct) scr_direct_finish(self); + else scr_fiber_finish(self); +} /* Extraction after the await's suspension: the promise has settled. */ static bool scr_coro_await_settled(ScrPromise *p) { @@ -2470,6 +2531,66 @@ ScrStr *scr_coro_await_str(ScrPromise *p) { void *scr_coro_await_ref(ScrPromise *p) { return scr_coro_await_settled(p) && p->payload ? p->retain_fn(p->payload) : NULL; } + +/* A direct await of a stackless body (scr_async_spawn_direct): a body + * still running gets the coroutine as its only awaiter, which its finish + * wakes as a settlement wakes a promise's waiter; a finished body is a + * settled promise, which takes one microtask turn. */ +bool scr_coro_direct_prepare(ScrFiber *self, ScrFiber *callee) { + if (!callee->done) { + callee->direct_awaiter = self; + return true; + } + return scr_coro_hop_needed(self); +} + +/* The result of a finished direct body: its promise's settlement, read as + * scr_coro_await_* reads it (a rejection re-throws), then the record goes. */ +static inline void scr_direct_destroy(ScrFiber *f) { + /* Only a rejection reason is left: values moved out, and nothing waits + * on or shares the promise. */ + if (f->promise->payload_kind != SCR_EXC_NONE) scr_promise_release_payload(f->promise); + scr_fiber_destroy(f); + scr_fibers_live--; +} + +void scr_coro_direct_void(ScrFiber *f) { + scr_coro_await_settled(f->promise); + scr_direct_destroy(f); +} +double scr_coro_direct_f64(ScrFiber *f) { + double v = scr_coro_await_settled(f->promise) ? f->promise->f64 : 0; + scr_direct_destroy(f); + return v; +} +bool scr_coro_direct_bool(ScrFiber *f) { + bool v = scr_coro_await_settled(f->promise) ? f->promise->b : false; + scr_direct_destroy(f); + return v; +} +/* String and reference results move out of the record (+1). */ +ScrStr *scr_coro_direct_str(ScrFiber *f) { + ScrPromise *p = f->promise; + ScrStr *v = NULL; + if (scr_coro_await_settled(p)) { + v = (ScrStr *)p->payload; + p->payload = NULL; + p->payload_kind = SCR_EXC_NONE; + } + scr_direct_destroy(f); + return v; +} +void *scr_coro_direct_ref(ScrFiber *f) { + ScrPromise *p = f->promise; + void *v = NULL; + if (scr_coro_await_settled(p)) { + v = p->payload; + p->payload = NULL; + p->payload_kind = SCR_EXC_NONE; + } + scr_direct_destroy(f); + return v; +} #endif /* One microtask hop: park the current fiber on the READY queue itself and @@ -3982,7 +4103,7 @@ static void scr_resume_fiber(ScrFiber *f) { scr_loop_reap = NULL; } #endif - if (f->done) { + if (f->done && !f->direct) { scr_promise_release(f->promise); scr_fiber_destroy(f); scr_fibers_live--; diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index 041d2ea030..827c303f97 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -109,6 +109,15 @@ double scr_coro_await_f64(ScrPromise *p); bool scr_coro_await_bool(ScrPromise *p); ScrStr *scr_coro_await_str(ScrPromise *p); /* +1 */ void *scr_coro_await_ref(ScrPromise *p); /* +1 via the stored retain */ +/* A direct await of a stackless body: its promise stays inside the fiber + * record the spawn returns, and the awaiter takes the result from it. */ +ScrFiber *scr_async_spawn_direct(void (*entry)(ScrFiber *, void *), void *argpack); +bool scr_coro_direct_prepare(ScrFiber *self, ScrFiber *callee); +void scr_coro_direct_void(ScrFiber *f); +double scr_coro_direct_f64(ScrFiber *f); +bool scr_coro_direct_bool(ScrFiber *f); +ScrStr *scr_coro_direct_str(ScrFiber *f); /* +1 */ +void *scr_coro_direct_ref(ScrFiber *f); /* +1 */ double scr_await_f64(ScrPromise *p); /* rejected promises re-throw */ bool scr_await_bool(ScrPromise *p); diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 0afb8981a1..c29126a4ab 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_v23(void) {} +void scr_runtime_abi_v24(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 cdf1061d94..94936a8aae 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_v23(void); +void scr_runtime_abi_v24(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/tests/corpus/7104-direct-await-order.ts b/tests/corpus/7104-direct-await-order.ts new file mode 100644 index 0000000000..ab4f438c46 --- /dev/null +++ b/tests/corpus/7104-direct-await-order.ts @@ -0,0 +1,96 @@ +// A stackless async body awaiting a stackless callee that suspends: +// results of each kind, callees that finish without suspending, nested +// chains, throws before and after the first suspension, and an uncaught +// rejection, against a ticker that logs every microtask turn. +interface Point { + x: number; + y: number; +} +const log: string[] = []; + +async function slowNumber(n: number, turns: number): Promise { + for (let i = 0; i < turns; i++) await null; + log.push(`slowNumber ${n} done`); + return n * 2; +} +async function maybeSuspend(n: number): Promise { + if (n > 0) await null; + log.push(`maybeSuspend ${n}`); + return n + 100; +} +async function slowFlag(n: number): Promise { + await null; + return n % 2 === 0; +} +async function slowLabel(s: string): Promise { + await null; + return `<${s}>`; +} +async function slowPoint(n: number): Promise { + await null; + return { x: n, y: n * n }; +} +async function slowVoid(tag: string): Promise { + await null; + log.push(`slowVoid ${tag}`); +} +async function throwsEarly(n: number): Promise { + if (n > 0) throw new RangeError(`early ${n}`); + await null; + return n; +} +async function throwsLate(n: number): Promise { + await null; + await null; + throw new TypeError(`late ${n}`); +} +async function chain(depth: number): Promise { + await null; + if (depth === 0) return 1; + return 1 + (await chain(depth - 1)); +} +function ticker(name: string, turns: number): Promise { + let p = Promise.resolve(); + for (let i = 0; i < turns; i++) p = p.then(() => void log.push(`${name} tick ${i}`)); + return p; +} + +async function values(): Promise { + const a = await slowNumber(1, 2); + const b = await maybeSuspend(0); + const c = await maybeSuspend(1); + const f = await slowFlag(4); + const s = await slowLabel("x"); + const p = await slowPoint(3); + await slowVoid("v"); + log.push(`values ${a} ${b} ${c} ${f} ${s} ${p.x + p.y}`); +} +async function errors(): Promise { + try { + await throwsEarly(1); + } catch (e) { + log.push(`errors caught ${(e as Error).name} ${(e as Error).message}`); + } + try { + await throwsLate(2); + } catch (e) { + log.push(`errors caught ${(e as Error).name} ${(e as Error).message}`); + } + log.push(`errors ok ${await throwsEarly(0)}`); +} +async function nested(): Promise { + log.push(`nested ${await chain(4)}`); +} +async function uncaught(): Promise { + await slowNumber(9, 1); + await throwsLate(9); + log.push("uncaught resolved"); +} + +process.on("unhandledRejection", (reason) => console.log(`unhandled ${(reason as Error).message}`)); +async function main(): Promise { + void uncaught(); + await Promise.all([ticker("t", 10), values(), ticker("u", 3), errors(), nested()]); + for (const line of log) console.log(line); +} +void main(); diff --git a/tests/corpus/7105-direct-await-context.ts b/tests/corpus/7105-direct-await-context.ts new file mode 100644 index 0000000000..c5a6bd40ec --- /dev/null +++ b/tests/corpus/7105-direct-await-context.ts @@ -0,0 +1,34 @@ +// AsyncLocalStorage across a stackless body's awaits of stackless callees: +// each callee inherits its caller's store at the call, a store a callee +// enters after its first await stays inside it, and run() scopes survive +// the awaits. +import { AsyncLocalStorage } from "node:async_hooks"; + +const store = new AsyncLocalStorage(); +const log: string[] = []; + +async function read(tag: string): Promise { + await null; + return `${tag}=${store.getStore() ?? "none"}`; +} +async function enters(tag: string): Promise { + await null; + store.enterWith(`inner-${tag}`); + await null; + return `${tag}=${store.getStore() ?? "none"}`; +} +async function body(name: string): Promise { + log.push(await read(`${name}.first`)); + log.push(await enters(`${name}.enters`)); + log.push(await read(`${name}.after`)); +} + +async function main(): Promise { + await Promise.all([ + store.run("a", () => body("a")), + store.run("b", () => body("b")), + body("none"), + ]); + for (const line of log) console.log(line); +} +void main(); diff --git a/tests/harness/corpus-costs/7104-direct-await-order.ts.json b/tests/harness/corpus-costs/7104-direct-await-order.ts.json new file mode 100644 index 0000000000..17c78c9c9a --- /dev/null +++ b/tests/harness/corpus-costs/7104-direct-await-order.ts.json @@ -0,0 +1 @@ +{ "plain": 3100, "san": 4100 } diff --git a/tests/harness/corpus-costs/7105-direct-await-context.ts.json b/tests/harness/corpus-costs/7105-direct-await-context.ts.json new file mode 100644 index 0000000000..78ee9421a0 --- /dev/null +++ b/tests/harness/corpus-costs/7105-direct-await-context.ts.json @@ -0,0 +1 @@ +{ "plain": 2300, "san": 3100 } From 561f66cf5b70fdcfeed8cbf93dc8fac02cd981c9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 00:16:11 -0500 Subject: [PATCH 057/395] Reuse deferred initializer scans while only bindings are pending --- .../deferred-module-initializers.test.ts | 20 + .../lowering/deferred-module-initializers.ts | 352 +++++++++++++----- 2 files changed, 274 insertions(+), 98 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts index 53d6bac879..28f05bfce8 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts @@ -91,6 +91,26 @@ test("finds replaced blocks and references appended to an existing body in a lat expect(functions[0]!.body).toEqual([{ kind: "block", body: [value(7)], loc }]); }); +test("reuses the scan of an unchanged body while only bindings are pending", () => { + const { initializers, pending, lowerer } = fixture(); + const placeholder = block(loc); + const functions = [fn("init", [placeholder]), fn("use", [value(1)])]; + pending.set("registry", [{ loc, lower: () => [value(7)] }]); + expect(initializers.process(lowerer, functions)).toBe(false); + const walk = vi.spyOn(traversal, "everyStmtList"); + try { + expect(initializers.process(lowerer, functions)).toBe(false); + expect(walk).not.toHaveBeenCalled(); + // A statement added to the same list is found. + functions[1]!.body.push(read("registry")); + expect(initializers.process(lowerer, functions)).toBe(true); + expect(walk.mock.calls.filter(([body]) => body === functions[0]!.body)).toHaveLength(0); + } finally { + walk.mockRestore(); + } + expect(placeholder.body).toEqual([value(7)]); +}); + test("indexes new subtrees for other demanded initializers in the same pass", () => { const { initializers, pending, lowerer } = fixture(); const nestedLoc = { ...loc, start: 10, end: 11 }; diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts index b80eb7ac00..eb22b40855 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts @@ -12,6 +12,7 @@ import { } from "./lower-classes.js"; import { classPrototypeData } from "./class-prototypes.js"; import { bindingNeverReassigned } from "./binding-analysis.js"; +import { InternalCompilerError } from "../../errors.js"; /** A registry of existing class/function values allocates data, but does * not execute those bodies. Keep its initializer at its source position @@ -20,6 +21,10 @@ import { bindingNeverReassigned } from "./binding-analysis.js"; export class DeferredModuleInitializers { private readonly pending = new Map IrStmt[] }[]>(); private readonly registryGlobals = new Set(); + /** Every key ever deferred, pending or materialized. */ + private readonly deferredKeys = new Set(); + /** The last scan of each function (scanOf). */ + private readonly scans = new Map(); defer(lowerer: Lowerer, statement: ts.Statement): IrStmt | null { let key: string; @@ -126,123 +131,274 @@ export class DeferredModuleInitializers { process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean { if (this.pending.size === 0) return false; - const demanded = new Set(); - const classHelpers = new Map(); - for (const info of lowerer.classes.values()) { - if (info.staticDataHelper) classHelpers.set(info.staticDataHelper, `class:${info.def.name}`); - if (info.def.prototypeDataHelper) - classHelpers.set(info.def.prototypeDataHelper, `prototype:${info.def.name}`); - } - const seen = new Set(); - const byName = new Map(functions.map((fn) => [fn.name, fn])); - const inspectedHelpers = new Set(); - const usedHelpers: string[] = []; - const demandType = (type: IrType): void => { - if (seen.has(type)) return; - seen.add(type); - if (type.kind === "object" || type.kind === "classval") { - for (let info = lowerer.classes.get(type.className); info; info = info.base ?? undefined) { - const key = `class:${info.def.name}`; - if (this.pending.has(key)) demanded.add(key); - const prototype = `prototype:${info.def.name}`; - if (type.kind === "object" && this.pending.has(prototype)) demanded.add(prototype); - } - } else if (type.kind === "array") demandType(type.elem); - else if (type.kind === "union") lowerer.unions.get(type.unionId)?.arms.forEach(demandType); - else if (type.kind === "record") { - const shape = lowerer.shapes.get(type.shapeId); - shape?.fields.forEach((field) => demandType(field.type)); - if (shape?.indexValue) demandType(shape.indexValue); - } else if (type.kind === "func") { - type.params.forEach(demandType); - demandType(type.ret); - } else if (type.kind === "promise") demandType(type.inner); - }; - for (const fn of functions) { - // Prototype helpers are allocated eagerly while collecting metadata. - // Their base-helper calls must not make unused classes reachable. - if (classHelpers.has(fn.name)) continue; - fn.params.forEach((param) => demandType(param.type)); - demandType(fn.returnType); - everyStmtList(fn.body, { - stmt: () => true, - expr: (expr) => { - demandType(expr.type); - if (readsBinding(expr) && this.pending.has(expr.localId)) demanded.add(expr.localId); - if (expr.kind === "call") { - const owner = classHelpers.get(expr.callee); - if (owner) { - if (this.pending.has(owner)) demanded.add(owner); - usedHelpers.push(expr.callee); - } - } - return true; - }, - }); + // Scans keep only the reads of deferred bindings. + for (const key of this.pending.keys()) { + if (this.deferredKeys.has(key)) continue; + this.deferredKeys.add(key); + this.scans.clear(); } - while (usedHelpers.length) { - const name = usedHelpers.pop()!; - if (inspectedHelpers.has(name)) continue; - inspectedHelpers.add(name); - const fn = byName.get(name); - if (!fn) continue; - everyStmtList(fn.body, { - stmt: () => true, - expr: (expr) => { - demandType(expr.type); - if (readsBinding(expr) && this.pending.has(expr.localId)) demanded.add(expr.localId); - if (expr.kind === "call") { - const owner = classHelpers.get(expr.callee); - if (owner) { - if (this.pending.has(owner)) demanded.add(owner); - usedHelpers.push(expr.callee); - } - } - return true; - }, - }); + // Class and prototype keys follow the classes IR types reach, and passes + // change those inside statements they keep (a bag initializer gains a + // prototype). While only bindings are pending, the scans of unchanged + // statement lists are reused; SCRIPTC_CHECK_DEFERRED_SCANS=1 checks each + // such pass against fresh scans. + const bindingsOnly = ![...this.pending.keys()].some( + (key) => key.startsWith("class:") || key.startsWith("prototype:"), + ); + const scans = functions.map((fn) => this.scanOf(lowerer, fn, bindingsOnly)); + const demanded = this.demanded(lowerer, functions, scans); + if (CHECK_SCANS && bindingsOnly) { + const fresh = functions.map((fn) => scanFunction(lowerer, fn, this.deferredKeys)); + const expected = this.demanded(lowerer, functions, fresh); + const blocks = (list: readonly FunctionScan[]): PlaceholderBlock[] => + list.flatMap((scan) => scan.placeholders.filter((block) => block.body.length === 0)); + const reused = blocks(scans); + const live = blocks(fresh); + if ( + [...demanded].join("\0") !== [...expected].join("\0") || + reused.length !== live.length || + reused.some((block, index) => block !== live[index]) + ) + throw new InternalCompilerError( + "reused deferred initializer scans disagree with a fresh scan", + ); } if (demanded.size === 0) return false; - // Transforms replace blocks between waves, so index the live placeholders - // anew in each pass. Searching every body again for each initializer makes - // materializing large module registries quadratic in the program size. - const placeholders = new Map[]>(); - const locationKey = (loc: IrStmt["loc"]): string => - JSON.stringify([loc.file, loc.start, loc.end]); - const indexPlaceholders = (body: IrStmt[]): void => { - everyStmtList(body, { - stmt: (statement) => { - if (statement.kind === "block" && statement.body.length === 0) { - const key = locationKey(statement.loc); - const blocks = placeholders.get(key) ?? []; - blocks.push(statement); - placeholders.set(key, blocks); - } - return true; - }, - expr: () => true, - }); + // The placeholders come from the scans, which are fresh for every list a + // transform replaced between waves. Each is kept with the functions whose + // bodies hold it. Searching every body again for each initializer would + // make materializing large module registries quadratic in the program size. + const placeholders = new Map(); + const add = (block: PlaceholderBlock, fns: IrFunction[]): void => { + const key = locationKey(block.loc); + const blocks = placeholders.get(key) ?? []; + blocks.push({ block, fns }); + placeholders.set(key, blocks); }; - for (const fn of functions) indexPlaceholders(fn.body); + functions.forEach((fn, index) => { + for (const block of scans[index]!.placeholders) if (block.body.length === 0) add(block, [fn]); + }); for (const id of demanded) { const initialize = this.pending.get(id)!; this.pending.delete(id); for (const action of initialize) { const body = action.lower(); - let inserted = false; - for (const block of placeholders.get(locationKey(action.loc)) ?? []) { + const holders: IrFunction[] = []; + for (const { block, fns } of placeholders.get(locationKey(action.loc)) ?? []) { if (block.body.length === 0) { block.body.push(...body); - inserted = true; + holders.push(...fns); } } + if (holders.length === 0) continue; + // These bodies changed inside unchanged statement lists. + for (const fn of holders) this.scans.delete(fn); // An inserted body can introduce a placeholder needed by another // action in this pass. Unattached bodies are not part of the live IR. - if (inserted) indexPlaceholders(body); + for (const nested of scanStatements(body).placeholders) add(nested, holders); } } return demanded.size !== 0; } + + /** The pending keys the scanned functions reference, in the order the + * functions and their walks meet them. */ + private demanded( + lowerer: Lowerer, + functions: readonly IrFunction[], + scans: readonly FunctionScan[], + ): Set { + const demanded = new Set(); + const classHelpers = new Map(); + for (const info of lowerer.classes.values()) { + if (info.staticDataHelper) classHelpers.set(info.staticDataHelper, `class:${info.def.name}`); + if (info.def.prototypeDataHelper) + classHelpers.set(info.def.prototypeDataHelper, `prototype:${info.def.name}`); + } + const byName = new Map(functions.map((fn, index) => [fn.name, index])); + const usedHelpers: string[] = []; + const demand = (events: readonly ScanEvent[]): void => { + for (const event of events) { + if (event.kind === "read") { + if (this.pending.has(event.id)) demanded.add(event.id); + } else if (event.kind === "call") { + const owner = classHelpers.get(event.callee); + if (owner) { + if (this.pending.has(owner)) demanded.add(owner); + usedHelpers.push(event.callee); + } + } else { + for ( + let info = lowerer.classes.get(event.className); + info; + info = info.base ?? undefined + ) { + const key = `class:${info.def.name}`; + if (this.pending.has(key)) demanded.add(key); + const prototype = `prototype:${info.def.name}`; + if (event.kind === "object" && this.pending.has(prototype)) demanded.add(prototype); + } + } + } + }; + functions.forEach((fn, index) => { + // Prototype helpers are allocated eagerly while collecting metadata. + // Their base-helper calls must not make unused classes reachable. + if (classHelpers.has(fn.name)) return; + demand(scans[index]!.signature); + demand(scans[index]!.body); + }); + const inspectedHelpers = new Set(); + while (usedHelpers.length) { + const name = usedHelpers.pop()!; + if (inspectedHelpers.has(name)) continue; + inspectedHelpers.add(name); + const index = byName.get(name); + if (index !== undefined) demand(scans[index]!.body); + } + return demanded; + } + + /** What `fn` references that can demand an initializer, in the order a + * walk of it meets them. While only bindings are pending (`reuse`), a + * function's scan is reused while its statement list is the same array + * with the same length and the same first and last statements: passes that + * change a body either replace its list or add statements to it, and this + * pass forgets the scan of a function whose placeholder it fills. */ + private scanOf(lowerer: Lowerer, fn: IrFunction, reuse: boolean): FunctionScan { + const cached = this.scans.get(fn); + if ( + reuse && + cached !== undefined && + cached.statements === fn.body && + cached.length === fn.body.length && + cached.first === fn.body[0] && + cached.last === fn.body[fn.body.length - 1] + ) { + return cached; + } + const scan = scanFunction(lowerer, fn, this.deferredKeys); + this.scans.set(fn, scan); + return scan; + } +} + +type PlaceholderBlock = Extract; + +/** One reference a scan records: a class an IR type reaches (`object` also + * reaches its prototype), a binding read, or a call to a class data + * helper. */ +type ScanEvent = + | { kind: "object" | "classval"; className: string } + | { kind: "read"; id: string } + | { kind: "call"; callee: string }; + +interface FunctionScan { + statements: IrStmt[]; + length: number; + first: IrStmt | undefined; + last: IrStmt | undefined; + /** References of the parameter and return types. */ + signature: ScanEvent[]; + /** References of the body's expressions. */ + body: ScanEvent[]; + /** Empty blocks, in walk order: an initializer's position. */ + placeholders: PlaceholderBlock[]; +} + +/** Class data helpers (classStaticData, classPrototypeData). */ +const CLASS_HELPER_PREFIXES = ["%static.data.", "%prototype.data."]; + +/** SCRIPTC_CHECK_DEFERRED_SCANS=1 checks each pass that reuses scans against fresh scans. */ +const CHECK_SCANS = process.env["SCRIPTC_CHECK_DEFERRED_SCANS"] === "1"; + +function locationKey(loc: IrStmt["loc"]): string { + return JSON.stringify([loc.file, loc.start, loc.end]); +} + +/** The references of `fn` (FunctionScan). Each reference is kept at its + * first occurrence, except calls, and only reads of `deferred` keys. */ +function scanFunction( + lowerer: Lowerer, + fn: IrFunction, + deferred: ReadonlySet, +): FunctionScan { + const signature: ScanEvent[] = []; + const typeEvents = typeScanner(lowerer, signature); + for (const param of fn.params) typeEvents(param.type); + typeEvents(fn.returnType); + const { body, placeholders } = scanStatements(fn.body, lowerer, deferred); + return { + statements: fn.body, + length: fn.body.length, + first: fn.body[0], + last: fn.body[fn.body.length - 1], + signature, + body, + placeholders, + }; +} + +/** Records, once each, the classes the types it is given reach. */ +function typeScanner(lowerer: Lowerer, events: ScanEvent[]): (type: IrType) => void { + const seen = new Set(); + const classes = new Set(); + const visit = (type: IrType): void => { + if (seen.has(type)) return; + seen.add(type); + if (type.kind === "object" || type.kind === "classval") { + const key = `${type.kind}:${type.className}`; + if (!classes.has(key)) { + classes.add(key); + events.push({ kind: type.kind, className: type.className }); + } + } else if (type.kind === "array") visit(type.elem); + else if (type.kind === "union") lowerer.unions.get(type.unionId)?.arms.forEach(visit); + else if (type.kind === "record") { + const shape = lowerer.shapes.get(type.shapeId); + shape?.fields.forEach((field) => visit(field.type)); + if (shape?.indexValue) visit(shape.indexValue); + } else if (type.kind === "func") { + type.params.forEach(visit); + visit(type.ret); + } else if (type.kind === "promise") visit(type.inner); + }; + return visit; +} + +/** The references and placeholders of a statement list. Without a lowerer + * it collects only placeholders. */ +function scanStatements( + statements: IrStmt[], + lowerer?: Lowerer, + deferred?: ReadonlySet, +): { body: ScanEvent[]; placeholders: PlaceholderBlock[] } { + const body: ScanEvent[] = []; + const placeholders: PlaceholderBlock[] = []; + const typeEvents = lowerer ? typeScanner(lowerer, body) : null; + const reads = new Set(); + everyStmtList(statements, { + stmt: (statement) => { + if (statement.kind === "block" && statement.body.length === 0) placeholders.push(statement); + return true; + }, + expr: (expr) => { + if (typeEvents === null) return true; + typeEvents(expr.type); + if (readsBinding(expr) && deferred!.has(expr.localId) && !reads.has(expr.localId)) { + reads.add(expr.localId); + body.push({ kind: "read", id: expr.localId }); + } + // Every call, as the helpers are visited from a stack in call order. + if ( + expr.kind === "call" && + CLASS_HELPER_PREFIXES.some((prefix) => expr.callee.startsWith(prefix)) + ) + body.push({ kind: "call", callee: expr.callee }); + return true; + }, + }); + return { body, placeholders }; } /** A read of a binding's current value: a plain read, or an update From 321873b9576c9692d9193c2193b18a7da00d4ccc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 00:18:36 -0500 Subject: [PATCH 058/395] Compile JavaScript-path calls that Node runs --- .../builtin-apis/streams-and-readline.mdx | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 3 + .../src/frontend/lowering/builtins/atomics.ts | 16 +++-- .../src/frontend/lowering/builtins/numbers.ts | 18 ++--- .../frontend/lowering/builtins/readline.ts | 34 +++++++--- .../lowering/containers/string-and-regexp.ts | 36 ++++++++-- packages/compiler/src/ir/builtin-effects.ts | 3 + .../compiler/src/ir/builtin-signatures.ts | 3 + .../6862-readline-chained-listeners.cjs.json | 6 ++ ...6865-number-parseint-default-radix.ts.json | 6 ++ .../6866-number-parseint-untyped.js.json | 6 ++ .../6867-string-match-optional-regexp.js.json | 6 ++ ...-buffer-from-untyped-array-buffer.cjs.json | 6 ++ .../6869-atomics-untyped-arrays.cjs.json | 6 ++ packages/runtime/src/scr_bytes_io.c | 13 ++++ packages/runtime/src/scr_lib.h | 3 + packages/runtime/src/scr_shared.c | 68 +++++++++++++++++++ .../6862-readline-chained-listeners.cjs | 29 ++++++++ .../6865-number-parseint-default-radix.ts | 16 +++++ tests/corpus/6866-number-parseint-untyped.js | 9 +++ .../6867-string-match-optional-regexp.js | 20 ++++++ .../6868-buffer-from-untyped-array-buffer.cjs | 38 +++++++++++ tests/corpus/6869-atomics-untyped-arrays.cjs | 65 ++++++++++++++++++ .../6862-readline-chained-listeners.cjs.json | 1 + ...6865-number-parseint-default-radix.ts.json | 1 + .../6866-number-parseint-untyped.js.json | 1 + .../6867-string-match-optional-regexp.js.json | 1 + ...-buffer-from-untyped-array-buffer.cjs.json | 1 + .../6869-atomics-untyped-arrays.cjs.json | 1 + 29 files changed, 388 insertions(+), 29 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6862-readline-chained-listeners.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6865-number-parseint-default-radix.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6866-number-parseint-untyped.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6867-string-match-optional-regexp.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6869-atomics-untyped-arrays.cjs.json create mode 100644 tests/corpus/6862-readline-chained-listeners.cjs create mode 100644 tests/corpus/6865-number-parseint-default-radix.ts create mode 100644 tests/corpus/6866-number-parseint-untyped.js create mode 100644 tests/corpus/6867-string-match-optional-regexp.js create mode 100644 tests/corpus/6868-buffer-from-untyped-array-buffer.cjs create mode 100644 tests/corpus/6869-atomics-untyped-arrays.cjs create mode 100644 tests/harness/corpus-costs/6862-readline-chained-listeners.cjs.json create mode 100644 tests/harness/corpus-costs/6865-number-parseint-default-radix.ts.json create mode 100644 tests/harness/corpus-costs/6866-number-parseint-untyped.js.json create mode 100644 tests/harness/corpus-costs/6867-string-match-optional-regexp.js.json create mode 100644 tests/harness/corpus-costs/6868-buffer-from-untyped-array-buffer.cjs.json create mode 100644 tests/harness/corpus-costs/6869-atomics-untyped-arrays.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 a21f7a617f..046f7d6b5a 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -19,6 +19,7 @@ Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream. A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error. `node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile. + `on`, `once`, and `addListener` return the interface, so registrations chain. The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node. With `terminal: true`, or with no `terminal` option and `process.stdout` as the output when it is a TTY, the interface runs Node's terminal mode: the editing keys, the kill ring, undo and redo, history recording and recall, Tab completion through the completer, Ctrl+C, Ctrl+D, and Ctrl+Z, `TERM=dumb`, and the same escape sequences written to the output. The output's `'resize'` event does not redraw the line. diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8bb8bc3aab..6832117626 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1037,6 +1037,9 @@ export const LIB_FN_SYMS: Record = { "atomics.op": "scr_atomics_op", "atomics.notify": "scr_atomics_notify", "atomics.wait": "scr_atomics_wait", + "atomics.opDyn": "scr_atomics_op_dyn", + "atomics.waitDyn": "scr_atomics_wait_dyn", + "atomics.notifyDyn": "scr_atomics_notify_dyn", "worker.new": "scr_worker_new", "worker.isMainThread": "scr_context_is_main", "worker.threadId": "scr_context_thread_number", diff --git a/packages/compiler/src/frontend/lowering/builtins/atomics.ts b/packages/compiler/src/frontend/lowering/builtins/atomics.ts index 41e35a5119..5c72ed08ca 100644 --- a/packages/compiler/src/frontend/lowering/builtins/atomics.ts +++ b/packages/compiler/src/frontend/lowering/builtins/atomics.ts @@ -60,10 +60,14 @@ export function lowerAtomicsCall( // has a concrete typed-array binding. Validate the actual lowered storage. const array = lowerer.lowerExpr(source); const type = array.type; + // An untyped array (a JavaScript `workerData` or field): the runtime + // checks its kind and throws Node's TypeError for any other value. + const dynamic = type?.kind === "dyn"; if ( - type?.kind !== "bytes" || - !["i8", "u8", "i16", "u16", "i32", "u32"].includes(type.elem) || - ((member === "wait" || member === "notify") && type.elem !== "i32") + !dynamic && + (type?.kind !== "bytes" || + !["i8", "u8", "i16", "u16", "i32", "u32"].includes(type.elem) || + ((member === "wait" || member === "notify") && type.elem !== "i32")) ) { lowerer.noLowering( `Atomics.${member} over this array`, @@ -82,7 +86,7 @@ export function lowerAtomicsCall( if (member === "wait") return { kind: "libCall", - fn: "atomics.wait", + fn: dynamic ? "atomics.waitDyn" : "atomics.wait", args: [array, index, arg(2, 0), arg(3, Infinity)], type: STRING, loc, @@ -90,14 +94,14 @@ export function lowerAtomicsCall( if (member === "notify") return { kind: "libCall", - fn: "atomics.notify", + fn: dynamic ? "atomics.notifyDyn" : "atomics.notify", args: [array, index, arg(2, Infinity)], type: F64, loc, }; return { kind: "libCall", - fn: "atomics.op", + fn: dynamic ? "atomics.opDyn" : "atomics.op", args: [array, index, arg(2, 0), arg(3, 0), number(operation)], type: F64, loc, diff --git a/packages/compiler/src/frontend/lowering/builtins/numbers.ts b/packages/compiler/src/frontend/lowering/builtins/numbers.ts index 18ca0f1f3b..1ff35fbb51 100644 --- a/packages/compiler/src/frontend/lowering/builtins/numbers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/numbers.ts @@ -72,17 +72,14 @@ export function lowerNumberStaticCall( return { kind: "libCall", fn, args: [arg], type: BOOL, loc }; } if (member === "parseFloat" || member === "parseInt") { - const want = member === "parseFloat" ? 1 : 2; - if (call.arguments.nodes.length !== want) { - lowerer.noLowering( - `Number.${member} with ${call.arguments.nodes.length} argument${call.arguments.nodes.length === 1 ? "" : "s"}`, - call, - member === "parseInt" ? "pass an explicit radix: Number.parseInt(s, 10)" : undefined, - ); + const count = call.arguments.nodes.length; + if (count < 1 || count > (member === "parseFloat" ? 1 : 2)) { + lowerer.noLowering(`Number.${member} with ${count} argument${count === 1 ? "" : "s"}`, call); } // Number.parseFloat/Number.parseInt ARE the global parsers: over a // string argument they lower to the same static parse (parseInt with - // its explicit radix); only the coercing argument shapes are refused. + // its radix, or radix 0 when it is missing); only the coercing argument + // shapes are refused. const probed = tryLowerExpression(lowerer, call.arguments.nodes[0]!); if (probed !== null && isStringLike(lowerer, probed.type)) { const parsed = lowerStaticNumberGlobal(lowerer, member, call, call.arguments.nodes, loc); @@ -101,7 +98,10 @@ export function lowerNumberStaticCall( }; if (member === "parseFloat") return { kind: "libCall", fn: "num.parseFloat", args: [text], type: F64, loc }; - const radix = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, F64); + const radixNode = call.arguments.nodes[1]; + const radix: IrExpr = radixNode + ? lowerer.lowerExprExpecting(radixNode, F64) + : { kind: "numLit", value: 0, type: F64, loc }; return { kind: "libCall", fn: "num.parseInt", args: [text, radix], type: F64, loc }; } const argText = diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index ede65d7884..846b9a9261 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -353,7 +353,8 @@ function lowerPromptArg(lowerer: Lowerer, node: ts.Expression): { value: IrExpr; * - on/once/addListener for 'line' ((input: string) or no parameters), * 'error' ((err: Error) or no parameters), 'history' ((history: * string[]) or no parameters), and 'close'/'pause'/'resume'/'SIGINT'/ - * 'SIGTSTP'/'SIGCONT' (no parameters), as statements. + * 'SIGTSTP'/'SIGCONT' (no parameters); as an expression the call yields + * the interface, so registrations chain. * Everything else the lib declares fences member-qualified. Null for * non-Interface receivers. */ export function lowerReadlineMethodCall( @@ -505,14 +506,19 @@ export function lowerReadlineMethodCall( `${RL_EVENT_LIST} are the supported readline events`, ); } - if (!ts.isExpressionStatement(call.parent)) { - lowerer.unsupported( - "SC1090", - call, - "chaining readline listener registration (register each listener as its own statement)", - ); - } - const recv = receiver(); + // As an expression (`rl.on("line", f).on("close", g)`), the call + // registers the listener and yields the interface, as Node's on() does. + const recvLocal = ts.isExpressionStatement(call.parent) + ? undefined + : lowerer.declareHiddenLocal("%readlineListener", F64); + const saved: IrStmt[] = + recvLocal === undefined + ? [] + : [{ kind: "varDecl", localId: recvLocal.id, init: receiver(), loc }]; + const recv: IrExpr = + recvLocal === undefined + ? receiver() + : { kind: "varRef", localId: recvLocal.id, type: F64, loc }; const cb = lowerer.lowerExpr(args[1]!); const want = event === "line" @@ -547,13 +553,21 @@ export function lowerReadlineMethodCall( : `'${event}' listeners with parameters or a return value (use ())`, ); } - return { + const register: IrExpr = { kind: "libCall", fn: "rl.on", args: [recv, numLit(ev, loc), cb, boolLit(name === "once", loc)], type: VOID, loc, }; + if (recvLocal === undefined) return register; + return { + kind: "seqExpr", + stmts: [...saved, { kind: "exprStmt", expr: register, loc }], + result: { kind: "varRef", localId: recvLocal.id, type: F64, loc }, + type: F64, + loc, + }; } lowerer.noLowering( `readline.Interface.${name}`, diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 9567ee0f6c..38865093fe 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -919,12 +919,12 @@ export function lowerRegexMethodCall( })(); if ((receiverKind === "string" || nullableStringRecv) && name === "match") { const arg0 = call.arguments.nodes[0]; - if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern match: the SC2020 fence + if (!arg0 || !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern match: the SC2020 fence if (call.arguments.nodes.length !== 1) return null; const receiver = nullableStringRecv ? lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text) : lowerReceiver(); - const re = lowerer.lowerExprExpecting(arg0, { kind: "regex" }); + const re = lowerRegexPatternArgument(lowerer, arg0); // RegExpMatchArray | null maps to the string[] | null union by // itself; intern it directly when the checker's spelling doesn't — // or when it maps to something WIDER (an optional-chain call node @@ -982,10 +982,10 @@ export function lowerRegexMethodCall( // 0, exactly Node. Never throws. if (receiverKind === "string" && name === "search") { const arg0 = call.arguments.nodes[0]; - if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern form: the SC2020 fence + if (!arg0 || !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern form: the SC2020 fence if (call.arguments.nodes.length !== 1) return null; const receiver = lowerReceiver(); - const re = lowerer.lowerExprExpecting(arg0, { kind: "regex" }); + const re = lowerRegexPatternArgument(lowerer, arg0); return { kind: "regexIntrinsic", method: "search", receiver, args: [re], type: F64, loc }; } if (receiverKind === "string" && (name === "replace" || name === "replaceAll")) { @@ -1920,3 +1920,31 @@ export function lowerDynPrimitiveMethodCall( ); return seq(stmts, result); } + +/** Whether the pattern of `match` or `search` is a RegExp, or a RegExp that + * may be undefined (`s.match(patterns[i])` under noUncheckedIndexedAccess). */ +function isRegexPatternArgument(lowerer: Lowerer, node: ts.Expression): boolean { + const t = lowerer.mapTypeOf(lowerer.typeOf(node)); + if (t?.kind === "regex") return true; + if (t?.kind !== "union") return false; + const arms = lowerer.unions.get(t.unionId)?.arms ?? []; + return ( + arms.length === 2 && + arms.some((arm) => arm.kind === "regex") && + arms.some((arm) => arm.kind === "undefinedT") + ); +} + +/** The pattern of `match` or `search` as a RegExp: an undefined pattern is + * `new RegExp(undefined)`, the empty pattern, as in Node. */ +function lowerRegexPatternArgument(lowerer: Lowerer, node: ts.Expression): IrExpr { + const regex: IrType = { kind: "regex" }; + const empty: IrExpr = { + kind: "regexLit", + pattern: "(?:)", + flags: "", + type: regex, + loc: locOf(node), + }; + return lowerOptionalArgument(lowerer, node, regex, empty); +} diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index ca8c521186..482739db5a 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -97,6 +97,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "atomics.op", "atomics.wait", "atomics.notify", + "atomics.opDyn", + "atomics.waitDyn", + "atomics.notifyDyn", "arrayBuffer.byteLengthGetter", "arrayBuffer.viewU8C", "arrayBuffer.viewI8", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fa78765791..49683550e0 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1252,6 +1252,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "atomics.wait": { argTypes: [bytesOf("i32"), F64, F64, F64], result: STRING }, "atomics.notify": { argTypes: [bytesOf("i32"), F64, F64], result: F64 }, "atomics.op": { argTypes: [null, F64, F64, F64, F64], result: F64 }, + "atomics.opDyn": { argTypes: [DYN, F64, F64, F64, F64], result: F64 }, + "atomics.waitDyn": { argTypes: [DYN, F64, F64, F64], result: STRING }, + "atomics.notifyDyn": { argTypes: [DYN, F64, F64], result: F64 }, "worker.new": { argTypes: [F64, STRING, DYN], result: DYN }, "worker.isMainThread": { argTypes: [], result: BOOL }, "worker.threadId": { argTypes: [], result: F64 }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6862-readline-chained-listeners.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6862-readline-chained-listeners.cjs.json new file mode 100644 index 0000000000..5beb42eaf0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6862-readline-chained-listeners.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6862-readline-chained-listeners.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6865-number-parseint-default-radix.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6865-number-parseint-default-radix.ts.json new file mode 100644 index 0000000000..225f2c4626 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6865-number-parseint-default-radix.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6865-number-parseint-default-radix.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6866-number-parseint-untyped.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6866-number-parseint-untyped.js.json new file mode 100644 index 0000000000..f000b3a36d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6866-number-parseint-untyped.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6866-number-parseint-untyped.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6867-string-match-optional-regexp.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6867-string-match-optional-regexp.js.json new file mode 100644 index 0000000000..1d1105bab8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6867-string-match-optional-regexp.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6867-string-match-optional-regexp.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs.json new file mode 100644 index 0000000000..baa6e19354 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6869-atomics-untyped-arrays.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6869-atomics-untyped-arrays.cjs.json new file mode 100644 index 0000000000..f91624eb9e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6869-atomics-untyped-arrays.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6869-atomics-untyped-arrays.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 5532d96daa..ac8fad02e8 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1299,8 +1299,19 @@ ScrBytes *scr_buffer_concat_len_dyn(const ScrDyn *list, double total) { return scr_buffer_concat_dyn_impl(list, total, true); } +/* Buffer.from(arrayBuffer) over a checked value: a Buffer over all of the + * ArrayBuffer or SharedArrayBuffer that shares its memory, as in Node. A + * second argument would be the byte offset, and an encoding name converts + * to NaN, which is offset 0. */ +static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage) { + ScrBytes *out = scr_bytes_buffer_view(storage->v.handle.ptr, SCR_BYTES_U8, 0, false, 0); + if (out) out->is_buffer = true; + return out; +} + ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) { if (value->kind == SCR_DYN_STR) return scr_bytes_from_str(value->v.str, encoding); + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value); if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(SCR_BYTES_U8, value->v.bytes); if (value->kind == SCR_DYN_ARR) { return scr_buffer_from_array_like(value, (double)value->v.arr.len); @@ -1344,6 +1355,8 @@ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding if (value->kind == SCR_DYN_STR) return scr_buffer_from_str(value->v.str, encoding); if (value->kind == SCR_DYN_BYTES && !value->v.bytes->is_data_view) return scr_buffer_from_bytes(value->v.bytes); + /* A view of an ArrayBuffer shares its memory, so it never comes from the pool. */ + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value); ScrBytes *out = scr_buffer_from_dyn(value, encoding); if (!out) return NULL; out->is_buffer = true; diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index 1b859cea11..3ce18d7938 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -365,6 +365,9 @@ typedef struct ScrBytes ScrBytes; ScrStr *scr_atomics_wait(ScrBytes *, double, double, double); double scr_atomics_notify(ScrBytes *, double, double); double scr_atomics_op(ScrBytes *, double, double, double, double); +double scr_atomics_op_dyn(const ScrDyn *, double, double, double, double); +ScrStr *scr_atomics_wait_dyn(const ScrDyn *, double, double, double); +double scr_atomics_notify_dyn(const ScrDyn *, double, double); /* isatty(3) over an fd (0/1/2): a real boolean — false where Node's * non-TTY streams expose undefined (SEMANTICS.md). stdin_destroy is a diff --git a/packages/runtime/src/scr_shared.c b/packages/runtime/src/scr_shared.c index 2dac5a4d15..6505a71ab8 100644 --- a/packages/runtime/src/scr_shared.c +++ b/packages/runtime/src/scr_shared.c @@ -7,7 +7,9 @@ #include "scr_numeric.h" #include #include +#include #include +#include #include #if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) #ifdef _WIN32 @@ -360,3 +362,69 @@ double scr_atomics_notify(ScrBytes *bytes, double index, double count) { scr_wait_unlock(bucket); return notified; } + +/* V8's text for a value in the TypeErrors below: a typed array as + * [object Int8Array] and so on, an array as [object Array], an ArrayBuffer + * or other native object as #, an object as #, and + * other values as String() gives them (undefined and null by name). */ +static ScrStr *scr_atomics_value_text(const ScrDyn *value) { + char text[64]; + int n; + if (value->kind == SCR_DYN_BYTES) { + if (value->v.bytes->is_data_view) n = snprintf(text, sizeof text, "#"); + else n = snprintf(text, sizeof text, "[object %s]", scr_bytes_elem_name(value->v.bytes->elem)); + } else if (value->kind == SCR_DYN_ARR) { + n = snprintf(text, sizeof text, "[object Array]"); + } else if (value->kind == SCR_DYN_HANDLE) { + n = snprintf(text, sizeof text, "#<%s>", scr_dyn_handle_ops_of(value)->cls); + } else if (value->kind == SCR_DYN_OBJ) { + const char *name = scr_dyn_class_name(value); + n = snprintf(text, sizeof text, "#<%s>", name ? name : "Object"); + } else if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_class_name(value)) { + const ScrStr *name = scr_dyn_typed_ref_class_name(value); + n = snprintf(text, sizeof text, "#<%.*s>", (int)name->len, (const char *)name->data); + } else if (value->kind == SCR_DYN_UNDEF) { + n = snprintf(text, sizeof text, "undefined"); + } else if (value->kind == SCR_DYN_NULL) { + n = snprintf(text, sizeof text, "null"); + } else { + return scr_dyn_to_string(value, NULL); + } + return scr_str_new(text, n < (int)sizeof text ? (size_t)n : sizeof text - 1); +} + +/* The integer typed array an untyped Atomics argument holds, or NULL with + * Node's TypeError pending. `waitable` requires an Int32Array, as wait and + * notify do. */ +static ScrBytes *scr_atomics_dyn_array(const ScrDyn *value, bool waitable) { + if (value->kind == SCR_DYN_BYTES && !value->v.bytes->is_data_view) { + ScrBytesElem elem = value->v.bytes->elem; + bool integer = elem == SCR_BYTES_I8 || elem == SCR_BYTES_U8 || elem == SCR_BYTES_I16 || + elem == SCR_BYTES_U16 || elem == SCR_BYTES_I32 || elem == SCR_BYTES_U32; + if (waitable ? elem == SCR_BYTES_I32 : integer) return value->v.bytes; + } + ScrStr *shown = scr_atomics_value_text(value); + if (!shown) return NULL; + const char *tail = waitable ? " is not an int32 or BigInt64 typed array." : " is not an integer typed array."; + ScrStr *parts[2] = {shown, scr_str_new(tail, strlen(tail))}; + ScrStr *message = scr_str_concat_parts(parts, 2); + scr_str_release(parts[0]); + scr_str_release(parts[1]); + scr_throw_error(SCR_ERR_TYPE, message); + return NULL; +} + +double scr_atomics_op_dyn(const ScrDyn *array, double index, double value, double replacement, double operation) { + ScrBytes *bytes = scr_atomics_dyn_array(array, false); + return bytes ? scr_atomics_op(bytes, index, value, replacement, operation) : 0; +} + +ScrStr *scr_atomics_wait_dyn(const ScrDyn *array, double index, double expected, double timeout) { + ScrBytes *bytes = scr_atomics_dyn_array(array, true); + return bytes ? scr_atomics_wait(bytes, index, expected, timeout) : NULL; +} + +double scr_atomics_notify_dyn(const ScrDyn *array, double index, double count) { + ScrBytes *bytes = scr_atomics_dyn_array(array, true); + return bytes ? scr_atomics_notify(bytes, index, count) : 0; +} diff --git a/tests/corpus/6862-readline-chained-listeners.cjs b/tests/corpus/6862-readline-chained-listeners.cjs new file mode 100644 index 0000000000..78c05063f9 --- /dev/null +++ b/tests/corpus/6862-readline-chained-listeners.cjs @@ -0,0 +1,29 @@ +// Chained readline listener registration: on(), once() and addListener() +// return the interface, so `createInterface(...).on('line', f).on('close', g)` +// registers both listeners, in order, on the same interface; a chain's value +// is the interface itself. +'use strict'; +const readline = require('node:readline'); +const { Readable } = require('node:stream'); + +const lines = []; +readline + .createInterface({ input: Readable.from(['alpha\nbeta\n', 'gamma\n']), terminal: false }) + .on('line', (line) => lines.push(line)) + .once('line', (line) => console.log('first line', line)) + .on('close', () => console.log('lines', lines.length, lines.join(','))); + +const rl = readline.createInterface({ input: Readable.from(['one\ntwo']) }); +const same = rl.on('line', (line) => console.log('rl line', line)).addListener('close', () => { + console.log('rl close'); +}); +console.log('chain returns the interface', same === rl); + +let count = 0; +readline + .createInterface({ input: Readable.from(['x\ny\nz\n']) }) + .on('line', () => { + count++; + }) + .on('pause', () => console.log('pause after', count)) + .on('close', () => console.log('count', count)); diff --git a/tests/corpus/6865-number-parseint-default-radix.ts b/tests/corpus/6865-number-parseint-default-radix.ts new file mode 100644 index 0000000000..dc6d2a4ad6 --- /dev/null +++ b/tests/corpus/6865-number-parseint-default-radix.ts @@ -0,0 +1,16 @@ +// Number.parseInt without a radix parses as the global parseInt does: radix +// 10, or 16 after a 0x prefix, after leading whitespace and a sign, stopping +// at the first character that is not a digit. +const inputs = ["42", " 17px", "0x1F", "-0x10", "0X0a", "08", "010", "1e3", "", " ", "abc", " -0", "+12", "9007199254740993", "0b11", "0o17", "\t\n 99 bottles", "١٢", "-", "0x"]; +for (const text of inputs) { + const parsed = Number.parseInt(text); + console.log(JSON.stringify(text), Object.is(parsed, -0) ? "-0" : String(parsed), parsed === parseInt(text)); +} + +function depth(arg: string | undefined): number { + return Math.max(6, Number.parseInt(arg ?? "10")); +} +console.log("depth", depth(process.argv[2]), depth("21"), depth("3")); + +const sizes = "640x480".split("x").map((part) => Number.parseInt(part)); +console.log("sizes", sizes[0]! * sizes[1]!); diff --git a/tests/corpus/6866-number-parseint-untyped.js b/tests/corpus/6866-number-parseint-untyped.js new file mode 100644 index 0000000000..48223bd9bb --- /dev/null +++ b/tests/corpus/6866-number-parseint-untyped.js @@ -0,0 +1,9 @@ +// Number.parseInt over untyped values in JavaScript, with and without a +// radix: the argument converts with ToString first ("undefined", "null", +// "1,2" for an array), then parses as the global parseInt does. +const config = JSON.parse('{"port":"8080","hex":"0xff","list":[3,4],"flag":true,"none":null}'); +for (const key of ["port", "hex", "list", "flag", "none", "missing"]) { + const value = config[key]; + console.log(key, Number.parseInt(value), Number.parseInt(value, 16), Number.parseInt(value, 36)); +} +console.log("argv", Number.parseInt(process.argv[2])); diff --git a/tests/corpus/6867-string-match-optional-regexp.js b/tests/corpus/6867-string-match-optional-regexp.js new file mode 100644 index 0000000000..e847bda366 --- /dev/null +++ b/tests/corpus/6867-string-match-optional-regexp.js @@ -0,0 +1,20 @@ +// String match and search in JavaScript with a pattern that may be undefined +// (a Map lookup or an array read): a RegExp matches as usual, and an +// undefined pattern is `new RegExp(undefined)`, the empty pattern. +const text = 'agggtaaa tttaccct agggtaaa'; +const patterns = new Map([ + ['forward', /agggtaaa/g], + ['either', /agggtaaa|tttaccct/gi], + ['first', /t+a/], +]); +for (const name of ['forward', 'either', 'first', 'missing']) { + const re = patterns.get(name); + const m = text.match(re); + console.log(name, m === null ? null : m.length, m === null ? '' : JSON.stringify([...m]), text.search(re)); +} + +const list = [/a(g+)/, /x/]; +for (let i = 0; i < 3; i++) { + const m = text.match(list[i]); + console.log(i, m === null ? 'null' : `${m[0]} at ${m.index} ${m[1] ?? ''}`, text.search(list[i])); +} diff --git a/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs b/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs new file mode 100644 index 0000000000..f525f24cf6 --- /dev/null +++ b/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs @@ -0,0 +1,38 @@ +// Buffer.from over an untyped ArrayBuffer or SharedArrayBuffer (read from a +// Map, an untyped object field or a worker message): the Buffer covers the +// whole buffer and shares its memory, in both directions, as in Node. An +// encoding name in the second argument converts to byte offset 0. +'use strict'; +const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads'); + +if (isMainThread) { + const store = new Map(); + store.set('plain', new ArrayBuffer(6)); + store.set('shared', new SharedArrayBuffer(6)); + for (const key of ['plain', 'shared']) { + const storage = store.get(key); + const buf = Buffer.from(storage); + buf[1] = 0x41; + new Uint8Array(storage)[2] = 0x42; + console.log(key, buf.length, Buffer.isBuffer(buf), buf.toString('hex'), new Uint8Array(storage).join(',')); + const encoded = Buffer.from(storage, 'utf8'); + console.log(key, 'with encoding', encoded.length, encoded.toString('hex')); + } + + const holder = JSON.parse('{}'); + holder.data = new ArrayBuffer(3); + const view = Buffer.from(holder.data); + view.write('xyz'); + console.log('field', Buffer.from(holder.data).toString()); + + const sab = new SharedArrayBuffer(8); + const worker = new Worker(__filename, { workerData: sab }); + worker.on('message', (message) => { + const seen = Buffer.from(message.storage); + console.log('worker wrote', seen.toString('latin1'), new Uint8Array(sab).join(',')); + }); +} else { + const out = Buffer.from(workerData); + out.write('shared!'); + parentPort.postMessage({ storage: workerData }); +} diff --git a/tests/corpus/6869-atomics-untyped-arrays.cjs b/tests/corpus/6869-atomics-untyped-arrays.cjs new file mode 100644 index 0000000000..18fe073193 --- /dev/null +++ b/tests/corpus/6869-atomics-untyped-arrays.cjs @@ -0,0 +1,65 @@ +// Atomics over untyped arrays in JavaScript: a worker's workerData carries +// Int32Array and Uint8Array views of a SharedArrayBuffer, and load, store, +// add, sub, and, or, xor, exchange, compareExchange, wait and notify run on +// them as on typed bindings. Other values throw Node's +// TypeError, and an index outside the array Node's RangeError. The results +// go to an untyped array. +'use strict'; +const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads'); + +const lines = []; +function attempt(label, run) { + try { + run(); + lines.push(`${label} ok`); + } catch (error) { + lines.push(`${label} ${error.name} ${error.message}`); + } +} + +if (isMainThread) { + const sab = new SharedArrayBuffer(16); + const counters = new Int32Array(sab); + const worker = new Worker(__filename, { workerData: { counters, bytes: new Uint8Array(sab, 8, 8) } }); + worker.on('message', (message) => { + if (message === 'ready') { + Atomics.store(counters, 1, 42); + console.log('notified', Atomics.notify(counters, 1, 1) >= 0); + return; + } + for (const line of message.lines) console.log(line); + console.log('worker results', JSON.stringify(message.out)); + console.log('counters', Array.from(counters).join(',')); + }); +} else { + const counters = workerData.counters; + const bytes = workerData.bytes; + const out = []; + out.push(Atomics.add(counters, 0, 5), Atomics.sub(counters, 0, 2), Atomics.load(counters, 0)); + out.push(Atomics.or(counters, 2, 6), Atomics.and(counters, 2, 3), Atomics.xor(counters, 2, 1), Atomics.load(counters, 2)); + out.push(Atomics.exchange(counters, 3, 9), Atomics.compareExchange(counters, 3, 9, 11), Atomics.compareExchange(counters, 3, 0, 1)); + out.push(Atomics.add(bytes, 0, 300), Atomics.load(bytes, 0), Atomics.sub(bytes, 1, 1), Atomics.load(bytes, 1)); + out.push(Atomics.load(counters, 1.9), Atomics.load(counters, NaN)); + attempt('index past the end', () => { + Atomics.load(counters, 4); + }); + attempt('negative index', () => { + Atomics.add(bytes, -1, 1); + }); + const values = [[], {}, undefined, null, 5, 'text', new Float64Array(2), new Map(), new ArrayBuffer(4), new DataView(new ArrayBuffer(4))]; + for (const value of values) { + attempt('load', () => { + Atomics.load(value, 0); + }); + attempt('notify', () => { + Atomics.notify(value, 0); + }); + } + attempt('wait on Uint8Array', () => { + Atomics.wait(bytes, 0, 0, 0); + }); + out.push(Atomics.wait(counters, 1, 0, 0)); + parentPort.postMessage('ready'); + out.push(Atomics.wait(counters, 1, 0, 5000) === 'ok' || Atomics.load(counters, 1) === 42); + parentPort.postMessage({ out, lines }); +} diff --git a/tests/harness/corpus-costs/6862-readline-chained-listeners.cjs.json b/tests/harness/corpus-costs/6862-readline-chained-listeners.cjs.json new file mode 100644 index 0000000000..5c8f4dafd8 --- /dev/null +++ b/tests/harness/corpus-costs/6862-readline-chained-listeners.cjs.json @@ -0,0 +1 @@ +{ "plain": 1930, "san": 4280 } diff --git a/tests/harness/corpus-costs/6865-number-parseint-default-radix.ts.json b/tests/harness/corpus-costs/6865-number-parseint-default-radix.ts.json new file mode 100644 index 0000000000..ad095e0aff --- /dev/null +++ b/tests/harness/corpus-costs/6865-number-parseint-default-radix.ts.json @@ -0,0 +1 @@ +{ "plain": 1940, "san": 4550 } diff --git a/tests/harness/corpus-costs/6866-number-parseint-untyped.js.json b/tests/harness/corpus-costs/6866-number-parseint-untyped.js.json new file mode 100644 index 0000000000..03bfc0b22f --- /dev/null +++ b/tests/harness/corpus-costs/6866-number-parseint-untyped.js.json @@ -0,0 +1 @@ +{ "plain": 1830, "san": 3890 } diff --git a/tests/harness/corpus-costs/6867-string-match-optional-regexp.js.json b/tests/harness/corpus-costs/6867-string-match-optional-regexp.js.json new file mode 100644 index 0000000000..f6a23dfa9e --- /dev/null +++ b/tests/harness/corpus-costs/6867-string-match-optional-regexp.js.json @@ -0,0 +1 @@ +{ "plain": 1960, "san": 4170 } diff --git a/tests/harness/corpus-costs/6868-buffer-from-untyped-array-buffer.cjs.json b/tests/harness/corpus-costs/6868-buffer-from-untyped-array-buffer.cjs.json new file mode 100644 index 0000000000..a1967e5286 --- /dev/null +++ b/tests/harness/corpus-costs/6868-buffer-from-untyped-array-buffer.cjs.json @@ -0,0 +1 @@ +{ "plain": 2070, "san": 4110 } diff --git a/tests/harness/corpus-costs/6869-atomics-untyped-arrays.cjs.json b/tests/harness/corpus-costs/6869-atomics-untyped-arrays.cjs.json new file mode 100644 index 0000000000..114626b53b --- /dev/null +++ b/tests/harness/corpus-costs/6869-atomics-untyped-arrays.cjs.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 4710 } From 71dccbb8a837820ff56fad25d367fc76d3688810 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 00:31:48 -0500 Subject: [PATCH 059/395] Declare runtime ABI 24 in the native toolchain test's runtime pack fixture The toolchain integrity test builds a runtime pack manifest by hand. It still declared ABI 23 after the runtime ABI moved to 24, so the staging check rejected the pack instead of staging it. --- tests/harness/self-hosting-native-toolchain.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 6d028d597c..7da7626b7f 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -105,7 +105,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" }, + runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From 2d353e6bbe58f399e087fec007beb02138d44279 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 00:33:54 -0500 Subject: [PATCH 060/395] Compute Math.pow(2, n) for integer exponents from the result's bits --- .../src/backend/llvm/lib-filesystem.ts | 62 +++++++++++++++++++ .../corpus/6863-math-pow-base-two.ts.json | 6 ++ packages/runtime/src/scr_lib.c | 11 ++++ tests/corpus/6863-math-pow-base-two.ts | 51 +++++++++++++++ .../6863-math-pow-base-two.ts.json | 1 + 5 files changed, 131 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6863-math-pow-base-two.ts.json create mode 100644 tests/corpus/6863-math-pow-base-two.ts create mode 100644 tests/harness/corpus-costs/6863-math-pow-base-two.ts.json diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index 358b21c4f7..6d5fe652d6 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -969,6 +969,64 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll return host.emitGenericLibCall(e); } +/** Math.pow(2, n): for an integer n from -1074 to 1023 the result is the + * power of two whose bits are its exponent field (or, below -1022, one + * mantissa bit of a subnormal), the value pow returns. Other exponents call + * scr_math_pow. */ +function emitPowerOfTwo(host: LlvmEmitterContext, n: string): string { + const B = host.B; + const check = B.newLabel("pow2.check"); + const exact = B.newLabel("pow2.exact"); + const call = B.newLabel("pow2.call"); + const join = B.newLabel("pow2.join"); + const low = B.tmp(); + const high = B.tmp(); + const inRange = B.tmp(); + B.line(`${low} = fcmp oge double ${n}, ${f64Lit(-1074)}`); + B.line(`${high} = fcmp ole double ${n}, ${f64Lit(1023)}`); + B.line(`${inRange} = and i1 ${low}, ${high}`); + B.condBr(inRange, check, call); + B.startBlock(check); + const int = B.tmp(); + const back = B.tmp(); + const integral = B.tmp(); + B.line(`${int} = fptosi double ${n} to i32`); + B.line(`${back} = sitofp i32 ${int} to double`); + B.line(`${integral} = fcmp oeq double ${back}, ${n}`); + B.condBr(integral, exact, call); + B.startBlock(exact); + const normal = B.tmp(); + const biased = B.tmp(); + const biasedWide = B.tmp(); + const normalBits = B.tmp(); + const shift = B.tmp(); + const shiftWide = B.tmp(); + const subnormalBits = B.tmp(); + const bits = B.tmp(); + const power = B.tmp(); + B.line(`${normal} = icmp sge i32 ${int}, -1022`); + B.line(`${biased} = add i32 ${int}, 1023`); + B.line(`${biasedWide} = zext i32 ${biased} to i64`); + B.line(`${normalBits} = shl i64 ${biasedWide}, 52`); + B.line(`${shift} = add i32 ${int}, 1074`); + B.line(`${shiftWide} = zext i32 ${shift} to i64`); + B.line(`${subnormalBits} = shl i64 1, ${shiftWide}`); + B.line(`${bits} = select i1 ${normal}, i64 ${normalBits}, i64 ${subnormalBits}`); + B.line(`${power} = bitcast i64 ${bits} to double`); + const exactEnd = B.currentLabel(); + B.br(join); + B.startBlock(call); + host.declare(`declare double @scr_math_pow(double, double)`); + const called = B.tmp(); + B.line(`${called} = call double @scr_math_pow(double ${f64Lit(2)}, double ${n})`); + const callEnd = B.currentLabel(); + B.br(join); + B.startBlock(join); + const out = B.tmp(); + B.line(`${out} = phi double [ ${power}, %${exactEnd} ], [ ${called}, %${callEnd} ]`); + return out; +} + export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; if (e.fn === "math.min" || e.fn === "math.max") { @@ -986,6 +1044,10 @@ export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr): const v = host.emitExpr(e.args[0]!); return { name: emitTruncF64(host, v.name), type: e.type }; } + if (e.fn === "math.pow" && e.args[0]!.kind === "numLit" && e.args[0]!.value === 2) { + const v = host.emitExpr(e.args[1]!); + return { name: emitPowerOfTwo(host, v.name), type: e.type }; + } if (e.fn === "math.floor" || e.fn === "math.ceil") { const intr = e.fn === "math.floor" ? "floor" : "ceil"; const v = host.emitExpr(e.args[0]!); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6863-math-pow-base-two.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6863-math-pow-base-two.ts.json new file mode 100644 index 0000000000..4e09ac3623 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6863-math-pow-base-two.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6863-math-pow-base-two.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 3441e61425..ee4c345925 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -6821,6 +6821,17 @@ double scr_math_round(double x) { * powers, follow libm. */ double scr_math_pow(double base, double exponent) { if (isnan(exponent) || (isinf(exponent) && fabs(base) == 1.0)) return NAN; + /* 2 ** n for an integer n in the double range is an exact power of two, + * the value pow returns; its bits are the exponent alone. */ + if (base == 2.0 && exponent >= -1074.0 && exponent <= 1023.0) { + int32_t n = (int32_t)exponent; + if ((double)n == exponent) { + uint64_t bits = n >= -1022 ? (uint64_t)(n + 1023) << 52 : UINT64_C(1) << (n + 1074); + double result; + memcpy(&result, &bits, sizeof result); + return result; + } + } return pow(base, exponent); } diff --git a/tests/corpus/6863-math-pow-base-two.ts b/tests/corpus/6863-math-pow-base-two.ts new file mode 100644 index 0000000000..fa965382d5 --- /dev/null +++ b/tests/corpus/6863-math-pow-base-two.ts @@ -0,0 +1,51 @@ +// Math.pow(2, n) with a literal base of 2 and with a base of 2 in a variable, +// over every integer exponent from -1080 to 1030: subnormal results down to +// 2 ** -1074, zero below it, Infinity above 1023. Non-integer, signed-zero, +// infinite and NaN exponents, and bases next to 2, take the general path. +function bits(x: number): string { + const view = new DataView(new ArrayBuffer(8)); + view.setFloat64(0, x); + return view.getBigUint64(0).toString(16).padStart(16, "0"); +} + +function show(label: string, x: number): void { + console.log(label, bits(x), Object.is(x, -0) ? "-0" : String(x)); +} + +let base = 2; +if (process.argv.length > 99) base = 3; + +let literalDigest = 0n; +let variableDigest = 0n; +let mismatches = 0; +for (let n = -1080; n <= 1030; n++) { + const literal = Math.pow(2, n); + const variable = Math.pow(base, n); + literalDigest = (literalDigest * 31n + BigInt(`0x${bits(literal)}`)) & 0xffffffffffffffffn; + variableDigest = (variableDigest * 31n + BigInt(`0x${bits(variable)}`)) & 0xffffffffffffffffn; + if (!Object.is(literal, variable)) mismatches++; + if (n <= -1070 || (n >= -1025 && n <= -1019) || (n >= -2 && n <= 2) || (n >= 51 && n <= 54) || n >= 1021) { + show(`2 ** ${n}`, literal); + } +} +console.log("digests", literalDigest.toString(16), variableDigest.toString(16), "mismatches", mismatches); + +const exponents = [0.5, -0.5, 1.5, -0, 0, 1e-300, -1e-300, 1023.0000001, 1022.9999999, -1074.5, -1073.5, Infinity, -Infinity, NaN, 52.999999999999996, 2 ** 31, -(2 ** 31)]; +for (const e of exponents) { + show(`literal ${e}`, Math.pow(2, e)); + show(`variable ${e}`, Math.pow(base, e)); +} +for (const b of [-2, 2.0000000000000004, 1.9999999999999998, 4, 0.5]) { + show(`${b} ** 10`, Math.pow(b, 10)); + show(`${b} ** -3`, Math.pow(b, -3)); +} + +// In a loop that masks values, as a k-mer counter does. +let key = 0; +let total = 0; +for (let i = 0; i < 100000; i++) { + const length = (i % 18) + 1; + key = (key * 4 + (i % 4)) % Math.pow(2, 2 * length); + total += key - Math.floor(key / Math.pow(2, length)) * Math.pow(2, length); +} +console.log("masked total", total); diff --git a/tests/harness/corpus-costs/6863-math-pow-base-two.ts.json b/tests/harness/corpus-costs/6863-math-pow-base-two.ts.json new file mode 100644 index 0000000000..3b43cd6600 --- /dev/null +++ b/tests/harness/corpus-costs/6863-math-pow-base-two.ts.json @@ -0,0 +1 @@ +{ "plain": 2110, "san": 4860 } From 47ba594c3f687bee589c2137fd6995678f10eb52 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 00:34:51 -0500 Subject: [PATCH 061/395] Check every type a package type reaches before re-exporting it as a data shape The data-shape check answered a type that refers back to a type still being checked as a data shape and recorded that answer before the outer check finished. With `Branch { child: Tree; handle: Handle }` and `Tree { parent: Branch | null }`, checking Branch first recorded Tree as a data shape, and Tree was re-exported although Branch reaches an interface with a method. The check now walks every type reachable from the queried one before it records any of them as a data shape, and records the types on the path to a non-data type as non-data. --- .../npm-static-type-exports-host.test.ts | 59 +++++++++++++++ .../frontend/npm-static-type-exports-host.ts | 75 ++++++++++++------- 2 files changed, 109 insertions(+), 25 deletions(-) create mode 100644 packages/compiler/src/frontend/npm-static-type-exports-host.test.ts diff --git a/packages/compiler/src/frontend/npm-static-type-exports-host.test.ts b/packages/compiler/src/frontend/npm-static-type-exports-host.test.ts new file mode 100644 index 0000000000..e37283fe63 --- /dev/null +++ b/packages/compiler/src/frontend/npm-static-type-exports-host.test.ts @@ -0,0 +1,59 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, expect, test } from "vitest"; +import { nodeFrontendServices } from "./services-node.js"; +import { npmStaticTypeExportPlan } from "./npm-static-type-exports-host.js"; + +const dirs: string[] = []; +afterAll(() => { + for (const dir of dirs) rmSync(dir, { recursive: true, force: true }); +}); + +/** A program directory with one package whose .d.ts is `declarations`; + * returns the program's entry path. */ +function fixture(declarations: string): string { + const dir = mkdtempSync(join(tmpdir(), "scriptc-type-exports-")); + dirs.push(dir); + const lib = join(dir, "node_modules", "pkg", "lib"); + mkdirSync(lib, { recursive: true }); + writeFileSync( + join(dir, "node_modules", "pkg", "package.json"), + JSON.stringify({ name: "pkg", main: "./lib/index.js", types: "./lib/index.d.ts" }), + ); + writeFileSync(join(lib, "index.d.ts"), declarations); + writeFileSync(join(lib, "index.js"), "exports.grow = function grow(into) {};\n"); + writeFileSync(join(dir, "main.ts"), 'import { grow } from "pkg";\n'); + return join(dir, "main.ts"); +} + +/** The type names the runtime entry's typedefs re-export. */ +function reexported(entry: string): string[] { + const plan = npmStaticTypeExportPlan(nodeFrontendServices(), entry, ["pkg"]); + const block = [...plan.typedefs.values()].join("\n"); + return [...block.matchAll(/@typedef \{[^}]*\} (\w+)/g)].map((m) => m[1]!); +} + +test("only data shapes are re-exported", () => { + const entry = fixture(`export interface Point { x: number; y: number } +export interface Handle { close(): void } +export type Label = { text: string; at: Point }; +export type Callback = (point: Point) => void; +export interface Holder { handle: Handle } +export declare function grow(into: Point[]): void; +`); + expect(reexported(entry)).toEqual(["Point", "Label"]); +}); + +test("mutually recursive types are data shapes only when every type they reach is", () => { + // Branch is checked first and reaches Tree, which refers back to Branch: + // Tree is a data shape only if Branch is, and Branch holds a Handle. + const entry = fixture(`export interface Branch { child: Tree; handle: Handle } +export interface Tree { name: string; parent: Branch | null } +export interface Handle { close(): void } +export interface Node { value: number; next: List } +export interface List { head: Node | null } +export declare function grow(into: Tree[]): void; +`); + expect(reexported(entry)).toEqual(["Node", "List"]); +}); diff --git a/packages/compiler/src/frontend/npm-static-type-exports-host.ts b/packages/compiler/src/frontend/npm-static-type-exports-host.ts index 3c196e6a77..8a61ed62be 100644 --- a/packages/compiler/src/frontend/npm-static-type-exports-host.ts +++ b/packages/compiler/src/frontend/npm-static-type-exports-host.ts @@ -154,32 +154,57 @@ export function npmStaticTypeExportPlan( // package's declaration files do not declare is a global (`Array`, // `Record`, `Date`); a class, a value or an import from another package // is not a data shape. + // + // `referencesOf` answers one name: null when it is not a data shape by + // itself, otherwise the names it refers to. + type Ref = { file: string; name: string }; + const referencesOf = (file: string, name: string): Ref[] | null => { + const ex = declarationExports(file); + if (ex === null) return null; + const local = ex.locals.get(name); + if (local !== undefined) + return local.behavior ? null : local.references.map((r) => ({ file, name: r })); + if (ex.valueNames.has(name)) return null; + const from = ex.imports.get(name); + if (from !== undefined) { + const target = declarationTargetOf(file, from.specifier); + return target === null ? null : [{ file: target, name: from.local }]; + } + const reexport = typesOf(file, new Set()).types.get(name); + if (reexport !== undefined) return [{ file: reexport.file, name: reexport.decl.declared }]; + return []; + }; + // A name is a data shape when every name reachable from it is one by + // itself. `isData` walks everything reachable before it records any name + // as a data shape, so a type in a reference cycle is not recorded while + // another type in the cycle is still unchecked. const dataMemo = new Map(); - const isData = (file: string, name: string, visiting: Set): boolean => { - const key = `${file}\0${name}`; - const memo = dataMemo.get(key); - if (memo !== undefined) return memo; - if (visiting.has(key)) return true; // a recursive shape - visiting.add(key); - const answer = ((): boolean => { - const ex = declarationExports(file); - if (ex === null) return false; - const local = ex.locals.get(name); - if (local !== undefined) - return !local.behavior && local.references.every((r) => isData(file, r, visiting)); - if (ex.valueNames.has(name)) return false; - const from = ex.imports.get(name); - if (from !== undefined) { - const target = declarationTargetOf(file, from.specifier); - return target !== null && isData(target, from.local, visiting); + const isData = (file: string, name: string): boolean => { + const keyOf = (ref: Ref): string => `${ref.file}\0${ref.name}`; + // Each reached name, by key, with the key of the name it was reached from. + const reachedFrom = new Map([[keyOf({ file, name }), null]]); + const queue: Ref[] = [{ file, name }]; + for (let i = 0; i < queue.length; i++) { + const ref = queue[i]!; + const key = keyOf(ref); + const memo = dataMemo.get(key); + if (memo === true) continue; + const next = memo === false ? null : referencesOf(ref.file, ref.name); + if (next === null) { + // Every name on the path to this one reaches a non-data type. + for (let k: string | null = key; k !== null; k = reachedFrom.get(k) ?? null) + dataMemo.set(k, false); + return false; } - const reexport = typesOf(file, new Set()).types.get(name); - if (reexport !== undefined) return isData(reexport.file, reexport.decl.declared, visiting); - return true; - })(); - visiting.delete(key); - dataMemo.set(key, answer); - return answer; + for (const target of next) { + const targetKey = keyOf(target); + if (reachedFrom.has(targetKey)) continue; + reachedFrom.set(targetKey, key); + queue.push(target); + } + } + for (const key of reachedFrom.keys()) dataMemo.set(key, true); + return true; }; for (const pkg of packages) { const declared = resolveBareModule(entryPath, pkg, "types-only"); @@ -198,7 +223,7 @@ export function npmStaticTypeExportPlan( ({ decl, file }) => !values.has(decl.name) && !declaredByRuntime.has(decl.name) && - isData(file, decl.declared, new Set()), + isData(file, decl.declared), ) .map(({ decl }) => decl); if (exported.length === 0) continue; From 140b1f397fa6c293ed7d06ee9405a11e89ee0af0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 00:35:17 -0500 Subject: [PATCH 062/395] Reject DiffieHellman secrets of 0, 1 and p - 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit computeSecret now fails with ERR_CRYPTO_OPERATION_FAILED ("Failed to compute shared secret") when the exponentiation yields 0, 1 or p - 1, as Node's OpenSSL derivation does: such a secret is predictable, from a peer key of small order or a composite modulus. 6708 covers a secret of 1 and of p - 1 (peer keys of order 3 and 6 modulo a 512-bit prime with p ≡ 1 mod 6) and of 0 (a peer key sharing the factor of a square modulus). --- packages/runtime/src/scr_dh.c | 7 +++++-- tests/corpus/6708-dh-errors.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/scr_dh.c b/packages/runtime/src/scr_dh.c index c514b45ba6..351d97a0f3 100644 --- a/packages/runtime/src/scr_dh.c +++ b/packages/runtime/src/scr_dh.c @@ -1058,7 +1058,9 @@ static bool sd_derives(const ScDh *d) { } /* computeSecret(key, inputEncoding, outputEncoding): key^priv mod p, - * padded to the prime's length. */ + * padded to the prime's length. A secret of 0, 1 or p - 1 is predictable + * (a peer key of small order, or a composite modulus), so it fails like + * OpenSSL's derivation does. */ static ScrDyn *sd_compute_secret(ScDh *d, const ScrDyn *key, const ScrDyn *in_enc, const ScrDyn *out_enc) { ScrBytes *bytes = ca_key_bytes(SD_SUBJECT, key, in_enc); if (!bytes) return NULL; @@ -1073,7 +1075,8 @@ static ScrDyn *sd_compute_secret(ScDh *d, const ScrDyn *key, const ScrDyn *in_en sd_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYLEN", "Supplied key is too small"); } else if (mbedtls_mpi_cmp_mpi(&peer, &bound) >= 0) { sd_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYLEN", "Supplied key is too large"); - } else if (!d->has_priv || (!d->group && !sd_derives(d)) || !sd_exp(&secret, &peer, &d->priv, &d->p)) { + } else if (!d->has_priv || (!d->group && !sd_derives(d)) || !sd_exp(&secret, &peer, &d->priv, &d->p) || + mbedtls_mpi_cmp_int(&secret, 1) <= 0 || mbedtls_mpi_cmp_mpi(&secret, &bound) == 0) { sd_operation_failed("Failed to compute shared secret"); } else { size_t len = mbedtls_mpi_size(&d->p); diff --git a/tests/corpus/6708-dh-errors.js b/tests/corpus/6708-dh-errors.js index e092da7277..0c711c96da 100644 --- a/tests/corpus/6708-dh-errors.js +++ b/tests/corpus/6708-dh-errors.js @@ -145,3 +145,31 @@ attempt("p 23 generator 30", () => loose(crypto.createDiffieHellman(Buffer.from([23]), 30).setPrivateKey(Buffer.from([2]))).generateKeys(), ); attempt("p 9", () => loose(crypto.createDiffieHellman(Buffer.from([9])).setPrivateKey(Buffer.from([3]))).generateKeys()); + +// Secrets of 0, 1 and p - 1 fail like any other derivation failure: a peer +// key of order 3 or 6 modulo a prime with p ≡ 1 (mod 6), and a peer key +// sharing the factor of a square modulus. +const smooth = Buffer.from( + "ce306415e770bb1cde0380e3c5cf4e8110b5c8e82ca36b1d9074fa1e9d1ed41b0ae295f19288302647d2da0003fcac6fa21d8953afce2c30171844815ec7d6df", + "hex", +); +const orderThree = Buffer.from( + "6be4b47bb9f8143334dab6349ac191ef7208ae3d5e684a2c318d43acc45ebd2a27d79b075c6e14a4e6a75899d98ce05af71ef05e1869b1add76d1622ab5e7b1d", + "hex", +); +const orderSix = Buffer.from( + "624baf9a2d78a6e9a928caaf2b0dbc919ead1aaace3b20f15ee7b671d8c016f0e30afaea361a1b81612b81662a6fcc14aafe98f597647a823fab2e5eb3695bc2", + "hex", +); +function withPrivateKey(modulus, key) { + const dh = crypto.createDiffieHellman(modulus); + dh.setPrivateKey(Buffer.from([key])); + return dh; +} +attempt("secret 1", () => withPrivateKey(smooth, 3).computeSecret(orderThree).toString("hex")); +attempt("secret p - 1", () => withPrivateKey(smooth, 3).computeSecret(orderSix).toString("hex")); +attempt("secret of order 3, key 4", () => withPrivateKey(smooth, 4).computeSecret(orderThree).toString("hex").slice(0, 16)); +const factor = BigInt("0xdf22630cec2362c510f444af6784da7a3a714ed787eb3d6bf9158d18c087cc5d"); +const square = Buffer.from((factor * factor).toString(16).padStart(128, "0"), "hex"); +const factorKey = Buffer.from(factor.toString(16).padStart(128, "0"), "hex"); +attempt("secret 0", () => withPrivateKey(square, 2).computeSecret(factorKey).toString("hex")); From 8bbccee4f61efe8912096dbbf953f6b8f2853596 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 00:37:37 -0500 Subject: [PATCH 063/395] Read and write shared typed-array elements inline --- .../compiler/src/backend/llvm/expr-bytes.ts | 114 +++++++++++++---- .../test/bytes-element-emission.test.ts | 16 ++- .../6864-shared-typed-array-elements.ts.json | 6 + .../6864-shared-typed-array-elements.ts | 120 ++++++++++++++++++ .../6864-shared-typed-array-elements.ts.json | 1 + 5 files changed, 225 insertions(+), 32 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6864-shared-typed-array-elements.ts.json create mode 100644 tests/corpus/6864-shared-typed-array-elements.ts create mode 100644 tests/harness/corpus-costs/6864-shared-typed-array-elements.ts.json diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index 485b7a6b33..d36001bf6a 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -176,10 +176,10 @@ export function emitBytesLength( return { name: out, type: F64 }; } -/** Worker programs: a view of a SharedArrayBuffer (`shared` is fixed before - * script code can see the view) is read and written by the runtime under - * its shared-memory lock; every other buffer belongs to this thread and - * takes the ordinary inline path. The test follows the index check: a +/** Worker programs: an element of a view of a SharedArrayBuffer (`shared` is + * fixed before script code can see the view) is read and written with one + * relaxed atomic access of its width, as the runtime's shared accessors do; + * every other buffer belongs to this thread and takes the ordinary path. The test follows the index check: a * view's length never changes, so both paths use the one checked integer * index. Branches to the returned label for a thread-owned buffer and * continues in the shared block. */ @@ -276,8 +276,9 @@ function emitBytesLoad( * shared. An integer element joins as its 32-bit value and converts to a * number after the join, as the local load does, so compares and * ToInt32/ToUint32 of the result stay integer operations. The shared read - * is an ordinary call: another thread's write must stay visible to a loop - * that reads the same element again. */ + * is a monotonic atomic load, which LLVM neither hoists nor merges: another + * thread's write stays visible to a loop that reads the same element again. + * Float16 elements read through the runtime. */ function emitSharedOrLocalLoad( host: LlvmEmitterContext, elem: IrBytesElem, @@ -286,12 +287,18 @@ function emitSharedOrLocalLoad( ): LlValue { const B = host.B; const floating = elem === "f16" || elem === "f32" || elem === "f64"; - const fn = floating ? "scr_bytes_shared_get_f64" : "scr_bytes_shared_get_int"; - const ret = floating ? "double" : "i32"; - host.declare(`declare ${ret} @${fn}(ptr, ${host.sizeType})`); const local = emitSharedBytesSplit(host, receiver); - const shared = B.tmp(); - B.line(`${shared} = call ${ret} @${fn}(ptr ${receiver}, ${host.sizeType} ${idx}) cold`); + let shared: string; + if (elem === "f16") { + host.declare(`declare double @scr_bytes_shared_get_f64(ptr, ${host.sizeType})`); + shared = B.tmp(); + B.line( + `${shared} = call double @scr_bytes_shared_get_f64(ptr ${receiver}, ${host.sizeType} ${idx}) cold`, + ); + } else { + const loaded = emitSharedLoadAt(host, elem, host.emitBytesData(receiver), idx); + shared = loaded.uint32 ?? loaded.name; + } const sharedDone = B.newLabel("bytes.shared.done"); B.br(sharedDone); B.startBlock(local); @@ -316,6 +323,43 @@ function emitSharedOrLocalLoad( return { name: out, type: F64, uint32: u32 }; } +/** The element at a checked index of a shared view, given its data pointer: + * a relaxed atomic load of the element's width, aligned as shared views + * are. Not for Float16 elements. */ +function emitSharedLoadAt( + host: LlvmEmitterContext, + elem: IrBytesElem, + data: string, + idx: string, +): LlValue { + const B = host.B; + const size = BYTES_ELEMENT_SIZE[elem]; + const p = B.tmp(); + if (elem === "f32" || elem === "f64") { + const ty = elem === "f32" ? "float" : "double"; + const raw = B.tmp(); + B.line(`${p} = getelementptr inbounds ${ty}, ptr ${data}, ${host.sizeType} ${idx}`); + B.line(`${raw} = load atomic ${ty}, ptr ${p} monotonic, align ${size}`); + if (elem === "f64") return { name: raw, type: F64 }; + const out = B.tmp(); + B.line(`${out} = fpext float ${raw} to double`); + return { name: out, type: F64 }; + } + const bits = size * 8; + const raw = B.tmp(); + B.line(`${p} = getelementptr inbounds i${bits}, ptr ${data}, ${host.sizeType} ${idx}`); + B.line(`${raw} = load atomic i${bits}, ptr ${p} monotonic, align ${size}`); + const signed = elem === "i8" || elem === "i16" || elem === "i32"; + let wide = raw; + if (bits < 32) { + wide = B.tmp(); + B.line(`${wide} = ${signed ? "sext" : "zext"} i${bits} ${raw} to i32`); + } + const out = B.tmp(); + B.line(`${out} = ${signed ? "sitofp" : "uitofp"} i32 ${wide} to double`); + return { name: out, type: F64, uint32: wide }; +} + /** The element at a checked index of a thread-owned view, given its data * pointer. */ function emitBytesLoadAt( @@ -477,7 +521,7 @@ export function emitBytesSet( const done = B.newLabel("bytes.store.done"); const data = host.emitBytesData(receiver); const idx = emitBytesIndex(host, receiver, index, indexExpr, done, inBounds); - if (host.mod.workers) { + if (host.mod.workers && elem === "f16") { host.declare(`declare void @scr_bytes_shared_set(ptr, ${host.sizeType}, double)`); const local = emitSharedBytesSplit(host, receiver); B.line( @@ -498,32 +542,48 @@ export function emitBytesSet( elem === "f16" || elem === "f32" || elem === "f64" ? null : host.emitToUint32(value.name, expr, value.uint32); - const p = B.tmp(); if (elem === "f16") { // One round-to-nearest-even step from the double, in the runtime. host.declare(`declare zeroext i16 @scr_f16_from_f64(double)`); const narrowed = B.tmp(); + const p = B.tmp(); B.line(`${narrowed} = call zeroext i16 @scr_f16_from_f64(double ${value.name})`); B.line(`${p} = getelementptr inbounds i16, ptr ${data}, ${host.sizeType} ${idx}`); B.line(`store i16 ${narrowed}, ptr ${p}, align 1${bytesElementAlias(host)}`); - } else if (BYTES_ELEMENT_SIZE[elem] < 4) { - const bits = BYTES_ELEMENT_SIZE[elem] * 8; - const byte = B.tmp(); - B.line(`${byte} = trunc i32 ${stored!} to i${bits}`); - B.line(`${p} = getelementptr inbounds i${bits}, ptr ${data}, ${host.sizeType} ${idx}`); - B.line(`store i${bits} ${byte}, ptr ${p}, align 1${bytesElementAlias(host)}`); + B.br(done); + B.startBlock(done); + return; + } + // The element's bits as an LLVM value of the element's width. + let ty: string; + let operand: string; + if (BYTES_ELEMENT_SIZE[elem] < 4) { + ty = `i${BYTES_ELEMENT_SIZE[elem] * 8}`; + operand = B.tmp(); + B.line(`${operand} = trunc i32 ${stored!} to ${ty}`); } else if (elem === "f32") { - const narrowed = B.tmp(); - B.line(`${narrowed} = fptrunc double ${value.name} to float`); - B.line(`${p} = getelementptr inbounds float, ptr ${data}, ${host.sizeType} ${idx}`); - B.line(`store float ${narrowed}, ptr ${p}, align 1${bytesElementAlias(host)}`); + ty = "float"; + operand = B.tmp(); + B.line(`${operand} = fptrunc double ${value.name} to float`); } else if (elem === "f64") { - B.line(`${p} = getelementptr inbounds double, ptr ${data}, ${host.sizeType} ${idx}`); - B.line(`store double ${value.name}, ptr ${p}, align 1${bytesElementAlias(host)}`); + ty = "double"; + operand = value.name; } else { - B.line(`${p} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${idx}`); - B.line(`store i32 ${stored!}, ptr ${p}, align 1${bytesElementAlias(host)}`); + ty = "i32"; + operand = stored!; + } + if (host.mod.workers) { + // A shared view's element takes a relaxed atomic store of its width. + const local = emitSharedBytesSplit(host, receiver); + const p = B.tmp(); + B.line(`${p} = getelementptr inbounds ${ty}, ptr ${data}, ${host.sizeType} ${idx}`); + B.line(`store atomic ${ty} ${operand}, ptr ${p} monotonic, align ${BYTES_ELEMENT_SIZE[elem]}`); + B.br(done); + B.startBlock(local); } + const p = B.tmp(); + B.line(`${p} = getelementptr inbounds ${ty}, ptr ${data}, ${host.sizeType} ${idx}`); + B.line(`store ${ty} ${operand}, ptr ${p}, align 1${bytesElementAlias(host)}`); B.br(done); B.startBlock(done); } diff --git a/packages/compiler/test/bytes-element-emission.test.ts b/packages/compiler/test/bytes-element-emission.test.ts index 9a241b8541..505b519638 100644 --- a/packages/compiler/test/bytes-element-emission.test.ts +++ b/packages/compiler/test/bytes-element-emission.test.ts @@ -335,11 +335,17 @@ test("a body mutation keeps the byte loop on the general f64 path", () => { test("worker programs read and write shared views at the checked integer index", () => { const ll = emitLlvmModule({ ...fixture(), workers: true }); - // The shared path receives the index the inline path checked, as an integer, - // and integer elements come back as 32-bit values. - expect(ll).toMatch(/call i32 @scr_bytes_shared_get_int\(ptr %\w+, i64 %?\w+\) cold/); - expect(ll).toMatch(/call double @scr_bytes_shared_get_f64\(ptr %\w+, i64 %?\w+\) cold/); - expect(ll).toMatch(/call void @scr_bytes_shared_set\(ptr %\w+, i64 %?\w+, double %?[\w.]+\)/); + // The shared path uses the index the inline path checked, as an integer: + // each element is one relaxed atomic access of its width, aligned to it. + expect(ll).toMatch(/load atomic i8, ptr %\w+ monotonic, align 1/); + expect(ll).toMatch(/load atomic i32, ptr %\w+ monotonic, align 4/); + expect(ll).toMatch(/load atomic float, ptr %\w+ monotonic, align 4/); + expect(ll).toMatch(/store atomic i8 [%\w.]+, ptr %\w+ monotonic, align 1/); + expect(ll).toMatch(/store atomic i32 [%\w.]+, ptr %\w+ monotonic, align 4/); + expect(ll).toMatch(/store atomic float [%\w.]+, ptr %\w+ monotonic, align 4/); + expect(ll).not.toContain("@scr_bytes_shared_get_int"); + expect(ll).not.toContain("@scr_bytes_shared_get_f64"); + expect(ll).not.toContain("@scr_bytes_shared_set"); expect(ll).not.toContain("@scr_bytes_get_or_nan"); expect(ll).not.toContain("@scr_bytes_set("); // Both paths join as one integer, which converts to a number after the join. diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6864-shared-typed-array-elements.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6864-shared-typed-array-elements.ts.json new file mode 100644 index 0000000000..2a7203ef0f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6864-shared-typed-array-elements.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6864-shared-typed-array-elements.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6864-shared-typed-array-elements.ts b/tests/corpus/6864-shared-typed-array-elements.ts new file mode 100644 index 0000000000..ee3ecef323 --- /dev/null +++ b/tests/corpus/6864-shared-typed-array-elements.ts @@ -0,0 +1,120 @@ +// Element reads and writes of typed arrays over a SharedArrayBuffer, for every +// element kind, from the main thread and from a worker: each kind's +// conversion on write (wrapping, clamping, rounding to float32 and float16), +// NaN, -0 and the infinities, and values one thread writes and the other +// reads after a message. A worker loop that reads a flag without Atomics +// sees the main thread's write. +import { isMainThread, parentPort, Worker, workerData } from "node:worker_threads"; + +const KINDS = 10; + +const VALUES = [0, -0, 1, -1, 1.5, 2.5, -2.5, 127, 128, 255, 256, -129, 65535, 65536, 2 ** 31, -(2 ** 31) - 1, 2 ** 32 + 7, 0.1, 1e40, -1e-50, NaN, Infinity, -Infinity]; +const LENGTH = VALUES.length; + +interface Views { + i8: Int8Array; + u8: Uint8Array; + u8c: Uint8ClampedArray; + i16: Int16Array; + u16: Uint16Array; + i32: Int32Array; + u32: Uint32Array; + f16: Float16Array; + f32: Float32Array; + f64: Float64Array; +} + +/** One region of LENGTH elements per kind, each aligned to 8 bytes. */ +function viewsOf(sab: SharedArrayBuffer): Views { + const region = LENGTH * 8; + return { + i8: new Int8Array(sab, 0 * region, LENGTH), + u8: new Uint8Array(sab, 1 * region, LENGTH), + u8c: new Uint8ClampedArray(sab, 2 * region, LENGTH), + i16: new Int16Array(sab, 3 * region, LENGTH), + u16: new Uint16Array(sab, 4 * region, LENGTH), + i32: new Int32Array(sab, 5 * region, LENGTH), + u32: new Uint32Array(sab, 6 * region, LENGTH), + f16: new Float16Array(sab, 7 * region, LENGTH), + f32: new Float32Array(sab, 8 * region, LENGTH), + f64: new Float64Array(sab, 9 * region, LENGTH), + }; +} + +function show(x: number): string { + return Object.is(x, -0) ? "-0" : String(x); +} + +/** Writes VALUES (scaled by `scale`) through every view. */ +function writeAll(v: Views, scale: number): void { + for (let i = 0; i < LENGTH; i++) { + const x = VALUES[i]! * scale; + v.i8[i] = x; + v.u8[i] = x; + v.u8c[i] = x; + v.i16[i] = x; + v.u16[i] = x; + v.i32[i] = x; + v.u32[i] = x; + v.f16[i] = x; + v.f32[i] = x; + v.f64[i] = x; + } +} + +/** Every element of every view, read one at a time, and a sum per view. */ +function readAll(v: Views): string[] { + const lines: string[] = []; + const row = (kind: string, read: (i: number) => number): void => { + const cells: string[] = []; + for (let i = 0; i < LENGTH; i++) cells.push(show(read(i))); + lines.push(`${kind}: ${cells.join(" ")}`); + }; + row("Int8Array", (i) => v.i8[i]!); + row("Uint8Array", (i) => v.u8[i]!); + row("Uint8ClampedArray", (i) => v.u8c[i]!); + row("Int16Array", (i) => v.i16[i]!); + row("Uint16Array", (i) => v.u16[i]!); + row("Int32Array", (i) => v.i32[i]!); + row("Uint32Array", (i) => v.u32[i]!); + row("Float16Array", (i) => v.f16[i]!); + row("Float32Array", (i) => v.f32[i]!); + row("Float64Array", (i) => v.f64[i]!); + let total = 0; + for (let i = 0; i < LENGTH; i++) total += v.i32[i]! + v.u16[i]! + (v.u8c[i]! | 0); + lines.push(`integer total ${total}`); + return lines; +} + +if (isMainThread) { + const sab = new SharedArrayBuffer(KINDS * LENGTH * 8 + 16); + const views = viewsOf(sab); + const flag = new Int32Array(sab, KINDS * LENGTH * 8, 2); + writeAll(views, 1); + console.log("main wrote, main reads"); + for (const line of readAll(views)) console.log(line); + + const worker = new Worker(new URL(import.meta.url), { workerData: sab }); + worker.on("message", (lines: string[]) => { + if (lines.length === 1) { + console.log(lines[0]); + return; + } + console.log("main wrote, worker reads; worker wrote, main reads"); + for (const line of lines) console.log(line); + for (const line of readAll(views)) console.log(line); + flag[1] = 7; + flag[0] = 1; + }); +} else { + const sab = workerData as SharedArrayBuffer; + const views = viewsOf(sab); + const flag = new Int32Array(sab, KINDS * LENGTH * 8, 2); + const seen = readAll(views); + writeAll(views, -3); + parentPort!.postMessage(seen); + while (flag[0] === 0) { + // Spin until the main thread's write is visible. + } + parentPort!.postMessage([`worker saw the flag, then ${flag[1]}`]); +} diff --git a/tests/harness/corpus-costs/6864-shared-typed-array-elements.ts.json b/tests/harness/corpus-costs/6864-shared-typed-array-elements.ts.json new file mode 100644 index 0000000000..5ec5d60dc5 --- /dev/null +++ b/tests/harness/corpus-costs/6864-shared-typed-array-elements.ts.json @@ -0,0 +1 @@ +{ "plain": 2290, "san": 5070 } From b59da7529e2a04baa94402c5a1ced0194623451c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 00:44:22 -0500 Subject: [PATCH 064/395] Leave counters unbounded when a nested scan is not walked pass by pass highestCount walks a nested while loop from its bound only when the loop is a statement of the body or of its blocks and branches. A nested scan inside a labeled block or an expression reached the generic branch, which counted its increments once although the loop can repeat them. Such a loop now leaves the counter without a range. --- .../compiler/src/ir/integer-loop-facts.ts | 21 ++++++++++++++----- .../compiler/src/ir/integer-ranges.test.ts | 9 ++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/ir/integer-loop-facts.ts b/packages/compiler/src/ir/integer-loop-facts.ts index b075ef29a3..31edc2f48a 100644 --- a/packages/compiler/src/ir/integer-loop-facts.ts +++ b/packages/compiler/src/ir/integer-loop-facts.ts @@ -395,11 +395,15 @@ export function guardedLoopCounterFacts( /** The largest value counter `id` takes in one pass of `body`, which starts * at most at `entry`. The caller has checked that the body changes the - * counter only by positive literal increments outside nested loops, switch, - * exception and labeled regions, so those regions leave it unchanged, and - * every exit from a straight-line path leaves the body. An `if` whose - * condition does not write the counter caps it in the then branch by the - * condition's bounds; any other statement adds the increments it holds. */ + * counter only by positive literal increments outside switch, exception and + * labeled loop regions and loops other than nested while loops whose + * conditions bound it, and every exit from a straight-line path leaves the + * body. An `if` whose condition does not write the counter caps it in the + * then branch by the condition's bounds. A nested while loop that is a + * statement of the body, or of its blocks and branches, is walked from its + * bound; any other statement adds the increments it holds, and one that + * holds a loop writing the counter (in an expression or a labeled block) + * leaves it unbounded. */ function highestCount( body: readonly IrStmt[], id: string, @@ -416,6 +420,13 @@ function highestCount( }; const stmt = (x: IrStmt): boolean => { if (x.kind === "assign" && x.localId === id) total += increment(id, x.value) ?? 0; + // A loop reached here, inside an expression or a labeled region rather + // than through `walk`, can repeat its increments: no bound. + if ( + (x.kind === "while" || x.kind === "doWhile" || x.kind === "for" || x.kind === "forOf") && + writtenLocals([x]).has(id) + ) + total = Infinity; return everyStmtChild(x, expr, stmt); }; if (expression) expr(e as IrExpr); diff --git a/packages/compiler/src/ir/integer-ranges.test.ts b/packages/compiler/src/ir/integer-ranges.test.ts index b495eb814f..fcb0402756 100644 --- a/packages/compiler/src/ir/integer-ranges.test.ts +++ b/packages/compiler/src/ir/integer-ranges.test.ts @@ -863,6 +863,15 @@ test("a nested while loop may advance a counter its own condition bounds", () => ), ); expect(run(writing).after).toBeNull(); + // The same scan inside a labeled block or an expression is not walked + // pass by pass; its increments could repeat, so the counter stays unbounded. + expect(run({ loc, kind: "block", labels: ["scan"], body: [bounded] }).after).toBeNull(); + const inExpression: IrStmt = { + loc, + kind: "exprStmt", + expr: { loc, kind: "seqExpr", stmts: [bounded], result: num(0), type: F64 }, + }; + expect(run(inExpression).after).toBeNull(); }); test("an array length bounds while and for counters even when the body resizes the array", () => { From 728dc6cd51cdfdd45db3a8402481c82c49bda970 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 00:47:17 -0500 Subject: [PATCH 065/395] Count program-lifetime objects in memoryUsage and fix the runtime tests - process.memoryUsage(): heapTotal counts the region's chunks that threads hold, minus the chunks discarded workers returned, and heapUsed counts them less the tails threads left unused when they moved to a new chunk. - The region builds only where it can be used: Linux runtimes with the size-class allocator, which excludes the sanitizer runtimes (scriptc keeps 8-byte references there). The standalone allocator, cycle and young-nursery tests link again: the allocator test defines the scr_trap its region needs, and checks the region (below 32 GiB, aligned, zeroed, chunks larger than one, heap statistics, threads). - scr_plr_chunk has an initializer, like the other runtime globals. --- packages/runtime/src/scr_alloc.c | 42 +++++++++++++++-- packages/runtime/src/scr_alloc.test.c | 67 +++++++++++++++++++++++++++ packages/runtime/src/scr_rc.h | 2 +- 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index 4b29b4b51e..f2304aacef 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -1930,6 +1930,10 @@ void scr_mem_order_for_free(void **ptrs, size_t n, size_t offset) { * mallinfo2, the Darwin malloc zones, the Windows CRT heap, or the * sanitizer's allocator in sanitized builds). Walks the free lists, so * the cost grows with the number of free blocks. */ +#if defined(__linux__) && SCR_SMALL_ALLOC +static void scr_plr_heap_stats(size_t *held, size_t *used); +#endif + void scr_heap_stats(double *total, double *used) { size_t held = 0, idle = 0; #if SCR_SMALL_ALLOC @@ -1979,6 +1983,13 @@ void scr_heap_stats(double *total, double *used) { sys_held = s.committed; sys_used = s.allocated; } +#endif +#if defined(__linux__) && SCR_SMALL_ALLOC + /* Compact class instances (the program-lifetime region below). */ + size_t region_held, region_used; + scr_plr_heap_stats(®ion_held, ®ion_used); + sys_held += region_held; + sys_used += region_used; #endif *total = (double)(held + sys_held); *used = (double)(held - (idle < held ? idle : held) + sys_used); @@ -1986,7 +1997,8 @@ void scr_heap_stats(double *total, double *used) { /* ── program-lifetime region ──────────────────────────────────────────── * Instances of compact classes (llvm/classes.ts LlClassMeta.compact) in - * programs built for Linux without sanitizers live in one address range + * programs built for Linux without sanitizers (runtimes with the size-class + * allocator, SCR_SMALL_ALLOC) live in one address range * reserved for the whole process below 32 GiB, so a field that holds one * can store its address over 8 in 32 bits (0 stays NULL). The instances * are never freed. Each thread carves them from its own chunks of @@ -1994,7 +2006,7 @@ void scr_heap_stats(double *total, double *used) { * Pages are committed by first touch and start zeroed. Its strings are * arrays rather than literals: programs without compact classes then link * none of this, where literals would stay in the unit's merged strings. */ -#if defined(__linux__) && SIZE_MAX > UINT32_MAX +#if defined(__linux__) && SCR_SMALL_ALLOC #include #define SCR_PLR_CHUNK ((size_t)1 << 20) @@ -2012,10 +2024,13 @@ void scr_heap_stats(double *total, double *used) { static char *scr_plr_base; /* the range's start, once reserved */ static size_t scr_plr_size; static size_t scr_plr_top; /* atomic */ +/* Bytes of the chunks threads hold, and of the tails they left unused when + * they moved to a new chunk (scr_heap_stats); atomic. */ +static size_t scr_plr_held, scr_plr_waste; #if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) -_Thread_local ScrPlrChunk scr_plr_chunk; +_Thread_local ScrPlrChunk scr_plr_chunk = {NULL, NULL}; #else -ScrPlrChunk scr_plr_chunk; +ScrPlrChunk scr_plr_chunk = {NULL, NULL}; #endif /* Reserves `size` bytes at `at` exactly; NULL if refused. */ @@ -2072,6 +2087,7 @@ static __attribute__((noinline)) void scr_plr_reserve(void) { * chunk a thread takes registers scr_plr_thread_end. */ #define SCR_PLR_LINK 16 static _Thread_local char *scr_plr_mine; +static _Thread_local size_t scr_plr_mine_waste; /* this thread's part of scr_plr_waste */ static pthread_key_t scr_plr_end_key; static pthread_once_t scr_plr_end_once = PTHREAD_ONCE_INIT; static pthread_mutex_t scr_plr_free_lock = PTHREAD_MUTEX_INITIALIZER; @@ -2112,6 +2128,9 @@ static void scr_plr_thread_end(void *unused) { c = next; } static const char stats_line[] = "scr_sa: thread returned %zu MB of program-lifetime objects\n"; + __atomic_fetch_sub(&scr_plr_held, returned, __ATOMIC_RELAXED); + __atomic_fetch_sub(&scr_plr_waste, scr_plr_mine_waste, __ATOMIC_RELAXED); + scr_plr_mine_waste = 0; const char *stats = getenv("SCR_SA_STATS"); if (stats && stats[0] == '1') fprintf(stderr, stats_line, returned >> 20); } @@ -2162,6 +2181,14 @@ static __attribute__((noinline)) void *scr_plr_refill(size_t n) { #ifdef SCR_SA_TRIM scr_plr_own(p, chunk); #endif + if (scr_plr_chunk.next) { + size_t left = (size_t)(scr_plr_chunk.limit - scr_plr_chunk.next); + __atomic_fetch_add(&scr_plr_waste, left, __ATOMIC_RELAXED); +#ifdef SCR_SA_TRIM + scr_plr_mine_waste += left; +#endif + } + __atomic_fetch_add(&scr_plr_held, chunk, __ATOMIC_RELAXED); scr_plr_chunk.next = p + SCR_PLR_LINK + n; scr_plr_chunk.limit = p + chunk; return p + SCR_PLR_LINK; @@ -2173,6 +2200,13 @@ void scr_plr_init(void) { if (!__atomic_load_n(&scr_plr_base, __ATOMIC_ACQUIRE)) scr_plr_reserve(); } +static void scr_plr_heap_stats(size_t *held, size_t *used) { + size_t h = __atomic_load_n(&scr_plr_held, __ATOMIC_RELAXED); + size_t w = __atomic_load_n(&scr_plr_waste, __ATOMIC_RELAXED); + *held = h; + *used = w < h ? h - w : 0; +} + void *scr_plr_alloc(size_t n) { n = (n + 7) & ~(size_t)7; char *p = scr_plr_chunk.next; diff --git a/packages/runtime/src/scr_alloc.test.c b/packages/runtime/src/scr_alloc.test.c index 2d8024c6df..e20c0733bf 100644 --- a/packages/runtime/src/scr_alloc.test.c +++ b/packages/runtime/src/scr_alloc.test.c @@ -10,6 +10,13 @@ static bool owned(const void *p) { return (uintptr_t)p - scr_sa.base < scr_sa.span; } #endif +/* The allocator unit needs only scr_trap from the rest of the runtime: the + * program-lifetime region stops the program through it. */ +_Noreturn void scr_trap(const char *msg) { + fputs(msg, stderr); + abort(); +} + static void fill(unsigned char *p, size_t n, unsigned char seed) { for (size_t i = 0; i < n; i++) p[i] = (unsigned char)(seed + i * 7); } @@ -962,6 +969,60 @@ static void check_discard(void) { } #endif +#if defined(__linux__) && SCR_SMALL_ALLOC +/* The program-lifetime region hands out zeroed, 8-byte aligned blocks below + * 32 GiB, in order from the calling thread's chunk; a block larger than a + * chunk gets chunks of its own. Heap statistics count the chunks. Builds + * with AddressSanitizer have no region (its shadow memory takes the range), + * and scriptc builds with sanitizers keep pointers instead. */ +static void check_program_lifetime_region(void) { + double total0, used0, total1, used1; + scr_heap_stats(&total0, &used0); + scr_plr_init(); + unsigned char *a = scr_plr_alloc(20), *b = scr_plr_alloc(40); + assert(((uintptr_t)a & 7) == 0 && b == a + 24); + assert((uintptr_t)a >= ((uintptr_t)4 << 30) && (uintptr_t)b + 40 <= ((uintptr_t)32 << 30)); + for (size_t i = 0; i < 40; i++) assert(b[i] == 0); + size_t n = (size_t)3 << 20; + unsigned char *big = scr_plr_alloc(n); + assert(((uintptr_t)big & 7) == 0 && (uintptr_t)big + n <= ((uintptr_t)32 << 30)); + for (size_t i = 0; i < n; i += 4096) assert(big[i] == 0); + fill(big, n, 3); + verify(big, n, 3); + scr_heap_stats(&total1, &used1); + assert(total1 - total0 >= (double)n && used1 - used0 >= (double)n && used1 <= total1); +} + +#ifdef SCR_WORKERS +/* Threads carve the region concurrently: every block is zeroed and keeps + * what its thread wrote. */ +#define REGION_THREADS 4u +#define REGION_BLOCKS 20000u +static unsigned char *region_blocks[REGION_THREADS][REGION_BLOCKS]; + +static void *region_alloc(void *arg) { + unsigned t = (unsigned)(uintptr_t)arg; + for (unsigned i = 0; i < REGION_BLOCKS; i++) { + unsigned char *p = scr_plr_alloc(24); + for (size_t k = 0; k < 24; k++) assert(p[k] == 0); + fill(p, 24, (unsigned char)(t * 31 + i)); + region_blocks[t][i] = p; + } + return NULL; +} + +static void check_program_lifetime_region_threads(void) { + pthread_t threads[REGION_THREADS]; + for (unsigned t = 0; t < REGION_THREADS; t++) + assert(pthread_create(&threads[t], NULL, region_alloc, (void *)(uintptr_t)t) == 0); + for (unsigned t = 0; t < REGION_THREADS; t++) assert(pthread_join(threads[t], NULL) == 0); + for (unsigned t = 0; t < REGION_THREADS; t++) + for (unsigned i = 0; i < REGION_BLOCKS; i++) + verify(region_blocks[t][i], 24, (unsigned char)(t * 31 + i)); +} +#endif +#endif + int main(void) { #if SCR_SMALL_ALLOC check_slice_growth(); /* first: its class must be untouched */ @@ -975,6 +1036,12 @@ int main(void) { #if SCR_SMALL_ALLOC && defined(__linux__) && defined(__GLIBC__) check_heap_huge_pages(); #endif +#if defined(__linux__) && SCR_SMALL_ALLOC + check_program_lifetime_region(); +#ifdef SCR_WORKERS + check_program_lifetime_region_threads(); +#endif +#endif #if defined(SCR_WORKERS) && SCR_SMALL_ALLOC /* A short discard delay, which stays at its start until check_discard_delay_grows. */ setenv("SCR_SA_DISCARD_DELAY_MS", "100", 1); diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 93fdd7946e..7a921b4511 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -308,7 +308,7 @@ void scr_cyc_free(void *obj); /* frees the block, header included */ void *scr_mem_realloc(void *p, size_t n); void *scr_rt_calloc(size_t n); /* emitted-code entry points */ -#if defined(__linux__) && SIZE_MAX > UINT32_MAX +#if defined(__linux__) && SCR_SMALL_ALLOC /* The program-lifetime region (scr_alloc.c): compact class instances of * programs built for Linux without sanitizers (llvm/emitter.ts * lowRegionTarget), which are never freed, at 8-byte aligned addresses below From f1e6addc819ecde9d205be4af5fd5dc2c67d830e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 00:51:27 -0500 Subject: [PATCH 066/395] Encode messages into one block sized to their contents An encoded message allocated a node array (four nodes of 184 bytes to start, trimmed when encoding ended), a property array per object, a copy of every key and string, and the encoder's visit, identity and stack arrays. A port that queues many messages kept all of it until delivery: a {id, name} object took 15 allocations and 1.9 KB, and most of postMessage's time went into page faults on that memory. - The encoder builds nodes, properties and visits in arrays that start on the stack (8 entries), keeps borrowed references to keys and strings, and copies the result into one allocation: the header, the nodes, the properties grouped by node, the typed records and the text. A node is 40 bytes; the per-kind fields share a union. ArrayBuffer contents stay in their own allocation. - Decoding uses stack arrays for messages of up to 8 nodes and allocates the storage table only when the message has ArrayBuffer contents. - Freeing a message frees the block, and walks the nodes only when the message holds ports, native handles or ArrayBuffer contents. - The publication check reads the root before collecting the graph, so a message whose root is not frozen allocates nothing for it. The same object message now takes 4 allocations and 302 bytes, and a number 3 allocations and 129 bytes. bench/channel-messages runs in 161 ms instead of 250 ms (Node: 340 ms), with a peak RSS of 45 MB instead of 115 MB, and bench/worker-messages in 273 ms instead of 348 ms. --- packages/runtime/src/scr_message.c | 518 ++++++++++++++++-------- packages/runtime/src/scr_message.test.c | 79 ++++ 2 files changed, 417 insertions(+), 180 deletions(-) diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index cd0e92740e..5e1bce37f0 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -21,7 +21,7 @@ typedef enum { /* A typed value published with @scriptc/threads (immortal), delivered as * a fresh typed-ref capsule over the same pointer. */ SCR_MESSAGE_TYPED_REF, - /* A transferred MessagePort: its channel end (+1), the side in number. */ + /* A transferred MessagePort: its channel end (+1) and side. */ SCR_MESSAGE_PORT, /* A native handle cloned through its unit's ScrDynHandleTransfer. */ SCR_MESSAGE_HANDLE, @@ -30,67 +30,109 @@ typedef enum { const ScrMessagePortOps *scr_message_port_ops = NULL; typedef struct { - char *key; + const char *key; size_t length; size_t value; bool hidden; /* an Error's non-enumerable or inherited property */ } ScrMessageProperty; +/* SCR_MESSAGE_TYPED_REF: the capsule's process-wide adapters and type key + * (vtables, adapters and type keys are shared by every thread). */ typedef struct { - ScrMessageKind kind; - double number; - unsigned char *data; - ScrSharedBytes *shared; - size_t length; - ScrMessageProperty *properties; - size_t count; - size_t capacity; - size_t storage; - size_t offset; - ScrBytesElem element; - bool data_view; - ScrDyn *published; /* SCR_MESSAGE_PUBLISHED: immortal root, owns nothing */ - ScrChannel *channel; /* SCR_MESSAGE_PORT */ - /* SCR_MESSAGE_HANDLE: the owning unit's hooks and its payload (+1). */ - const ScrDynHandleTransfer *transfer; - void *payload; - /* SCR_MESSAGE_TYPED_REF: the capsule's process-wide adapters and type key - * (vtables, adapters and type keys are shared by every thread). */ - struct { - void *ptr; - void *(*retain)(void *); - void (*release)(void *); - const char *type_key; - size_t type_key_len; - ScrDyn *(*materialize)(void *); - void (*commit)(void *, const ScrDyn *); - bool traced; - } typed; + void *ptr; + void *(*retain)(void *); + void (*release)(void *); + const char *type_key; + size_t type_key_len; + ScrDyn *(*materialize)(void *); + void (*commit)(void *, const ScrDyn *); + bool traced; +} ScrMessageTyped; + +typedef struct { + unsigned char kind; /* ScrMessageKind */ + unsigned char element; /* SCR_MESSAGE_BYTES: ScrBytesElem */ + bool data_view; /* SCR_MESSAGE_BYTES */ + uint32_t count; /* properties */ + const ScrMessageProperty *properties; + union { + double number; /* BOOL and NUMBER; ERROR: the error kind */ + /* STRING and BIGINT: the text, in the message block. STORAGE: the + * copied bytes (owned) or the shared backing (+1). */ + struct { + char *data; + ScrSharedBytes *shared; + size_t length; + } text; + /* BYTES and ARRAY_BUFFER: the STORAGE node; BYTES: the view. */ + struct { + size_t storage; + size_t offset; + size_t length; + } view; + size_t length; /* ARRAY */ + ScrDyn *published; /* PUBLISHED: immortal root, owns nothing */ + struct { + ScrChannel *channel; /* +1 */ + int side; + } port; + /* HANDLE: the owning unit's hooks and its payload (+1). */ + struct { + const ScrDynHandleTransfer *transfer; + void *payload; + } handle; + const ScrMessageTyped *typed; /* TYPED_REF, in the message block */ + size_t typed_index; /* TYPED_REF while encoding */ + } v; } ScrMessageNode; +/* A message is one allocation: this header, then the nodes, the + * properties, the typed records and the text (keys, strings and bigints), + * sized to the message. A queued message holds nothing else, apart from + * the bytes of STORAGE nodes. */ struct ScrMessage { ScrMessageNode *nodes; size_t count; - size_t capacity; + bool storage; /* has STORAGE nodes */ + bool releases; /* has STORAGE, PORT or HANDLE nodes, which own resources */ }; typedef struct { const ScrDyn *input; ScrDyn *view; ScrDyn *keys; + ScrStr *text; /* a STRING or BIGINT node's text, until the message is packed */ size_t cursor; bool uncaught; /* an Error encoded as an uncaught exception */ } ScrMessageVisit; +typedef struct { + ScrMessageProperty property; + size_t owner; +} ScrMessagePending; + typedef struct { const void *identity; unsigned domain; size_t node; } ScrMessageIdentity; +/* The encoder's arrays start inline, so a message of up to this many nodes + * and properties allocates only its block. */ +#define SCR_MESSAGE_INLINE 8 + typedef struct { - ScrMessage *message; + ScrMessageNode *nodes; ScrMessageVisit *visits; + size_t count; + size_t capacity; + ScrMessagePending *properties; /* in the order they were reached */ + size_t property_count; + size_t property_capacity; + ScrMessageTyped *typed; + size_t typed_count; + size_t typed_capacity; + size_t text; /* bytes of text in the message block */ ScrMessageIdentity *identities; size_t identity_count; size_t identity_capacity; @@ -99,8 +141,36 @@ typedef struct { size_t stack_capacity; const ScrDyn *transfer; /* the transfer list (a dyn array), or NULL */ bool uncaught; /* the next value is an uncaught exception or its cause */ + bool storage; + bool releases; + ScrMessageNode inline_nodes[SCR_MESSAGE_INLINE]; + ScrMessageVisit inline_visits[SCR_MESSAGE_INLINE]; + ScrMessagePending inline_properties[SCR_MESSAGE_INLINE]; + ScrMessageIdentity inline_identities[2 * SCR_MESSAGE_INLINE]; + size_t inline_stack[SCR_MESSAGE_INLINE]; } ScrMessageEncoder; +static void scr_message_encoder_init(ScrMessageEncoder *encoder, const ScrDyn *transfer, bool uncaught) { + encoder->nodes = encoder->inline_nodes; + encoder->visits = encoder->inline_visits; + encoder->count = 0; + encoder->capacity = SCR_MESSAGE_INLINE; + encoder->properties = encoder->inline_properties; + encoder->property_count = 0; + encoder->property_capacity = SCR_MESSAGE_INLINE; + encoder->typed = NULL; + encoder->typed_count = encoder->typed_capacity = 0; + encoder->text = 0; + encoder->identities = NULL; + encoder->identity_count = encoder->identity_capacity = 0; + encoder->stack = encoder->inline_stack; + encoder->depth = 0; + encoder->stack_capacity = SCR_MESSAGE_INLINE; + encoder->transfer = transfer; + encoder->uncaught = uncaught; + encoder->storage = encoder->releases = false; +} + static bool scr_message_listed(const ScrMessageEncoder *encoder, const ScrDyn *value) { if (!encoder->transfer) return false; for (size_t i = 0; i < encoder->transfer->v.arr.len; i++) { @@ -122,30 +192,42 @@ static size_t scr_message_grow(size_t capacity) { return capacity ? capacity * 2 : 4; } -static unsigned char *scr_message_copy(const void *data, size_t length) { +/* Moves an encoder array of count items to a new one of capacity items, + * freeing the old one unless it is the inline array. */ +static void *scr_message_regrow(void *items, size_t count, size_t capacity, size_t size, + const void *inline_items) { + void *grown = scr_message_alloc(capacity, size); + if (count) memcpy(grown, items, count * size); + if (items != inline_items) free(items); + return grown; +} + +static char *scr_message_copy(const void *data, size_t length) { if (length == SIZE_MAX) scr_trap("scriptc: message too large\n"); - unsigned char *copy = scr_message_alloc(length + 1, 1); + char *copy = scr_message_alloc(length + 1, 1); if (length) memcpy(copy, data, length); return copy; } +/* Reserves text (and its terminator) in the message block. */ +static void scr_message_text(ScrMessageEncoder *encoder, size_t length) { + if (length >= SIZE_MAX - encoder->text) scr_trap("scriptc: message too large\n"); + encoder->text += length + 1; +} + static size_t scr_message_node(ScrMessageEncoder *encoder) { - ScrMessage *message = encoder->message; - if (message->count == message->capacity) { - size_t capacity = scr_message_grow(message->capacity); - ScrMessageNode *nodes = scr_message_alloc(capacity, sizeof(*nodes)); - ScrMessageVisit *visits = scr_message_alloc(capacity, sizeof(*visits)); - if (message->count) { - memcpy(nodes, message->nodes, message->count * sizeof(*nodes)); - memcpy(visits, encoder->visits, message->count * sizeof(*visits)); - } - free(message->nodes); - free(encoder->visits); - message->nodes = nodes; - encoder->visits = visits; - message->capacity = capacity; - } - return message->count++; + if (encoder->count == encoder->capacity) { + size_t capacity = scr_message_grow(encoder->capacity); + encoder->nodes = scr_message_regrow(encoder->nodes, encoder->count, capacity, + sizeof(*encoder->nodes), encoder->inline_nodes); + encoder->visits = scr_message_regrow(encoder->visits, encoder->count, capacity, + sizeof(*encoder->visits), encoder->inline_visits); + encoder->capacity = capacity; + } + size_t id = encoder->count++; + memset(&encoder->nodes[id], 0, sizeof(encoder->nodes[id])); + memset(&encoder->visits[id], 0, sizeof(encoder->visits[id])); + return id; } static size_t scr_message_hash(const void *identity, unsigned domain) { @@ -171,7 +253,11 @@ static size_t scr_message_find(const ScrMessageEncoder *encoder, const void *ide static void scr_message_identify(ScrMessageEncoder *encoder, const void *identity, unsigned domain, size_t node) { if (!identity) return; - if (encoder->identity_count >= encoder->identity_capacity / 2) { + if (!encoder->identity_capacity) { + encoder->identity_capacity = sizeof encoder->inline_identities / sizeof encoder->inline_identities[0]; + encoder->identities = encoder->inline_identities; + memset(encoder->identities, 0, sizeof encoder->inline_identities); + } else if (encoder->identity_count >= encoder->identity_capacity / 2) { size_t capacity = scr_message_grow(encoder->identity_capacity); ScrMessageIdentity *entries = scr_message_alloc(capacity, sizeof(*entries)); for (size_t i = 0; i < encoder->identity_capacity; i++) { @@ -181,7 +267,7 @@ static void scr_message_identify(ScrMessageEncoder *encoder, const void *identit while (entries[slot].identity) slot = (slot + 1) & (capacity - 1); entries[slot] = entry; } - free(encoder->identities); + if (encoder->identities != encoder->inline_identities) free(encoder->identities); encoder->identities = entries; encoder->identity_capacity = capacity; } @@ -194,10 +280,8 @@ static void scr_message_identify(ScrMessageEncoder *encoder, const void *identit static void scr_message_push(ScrMessageEncoder *encoder, size_t node) { if (encoder->depth == encoder->stack_capacity) { size_t capacity = scr_message_grow(encoder->stack_capacity); - size_t *stack = scr_message_alloc(capacity, sizeof(*stack)); - if (encoder->depth) memcpy(stack, encoder->stack, encoder->depth * sizeof(*stack)); - free(encoder->stack); - encoder->stack = stack; + encoder->stack = scr_message_regrow(encoder->stack, encoder->depth, capacity, + sizeof(*encoder->stack), encoder->inline_stack); encoder->stack_capacity = capacity; } encoder->stack[encoder->depth++] = node; @@ -209,13 +293,14 @@ static size_t scr_message_storage(ScrMessageEncoder *encoder, const ScrBytes *by if (found != SIZE_MAX) return found; size_t node = scr_message_node(encoder); scr_message_identify(encoder, owner, SCR_MESSAGE_STORAGE + 1, node); - ScrMessageNode *storage = &encoder->message->nodes[node]; + ScrMessageNode *storage = &encoder->nodes[node]; storage->kind = SCR_MESSAGE_STORAGE; size_t size = scr_bytes_elem_size(owner->elem); if (owner->len > SIZE_MAX / size) scr_trap("scriptc: message too large\n"); - storage->length = owner->len * size; - if (owner->shared) storage->shared = scr_shared_retain(owner->shared); - else storage->data = scr_message_copy(owner->data, storage->length); + storage->v.text.length = owner->len * size; + if (owner->shared) storage->v.text.shared = scr_shared_retain(owner->shared); + else storage->v.text.data = scr_message_copy(owner->data, storage->v.text.length); + encoder->storage = encoder->releases = true; return node; } @@ -285,39 +370,43 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) SCR_RC_PUBLISHED(input->v.typed_ref.ptr)) { scr_sa_thread_shares(); encoder->visits[id].view = (ScrDyn *)input; - ScrMessageNode *node = &encoder->message->nodes[id]; + ScrMessageNode *node = &encoder->nodes[id]; + if (encoder->typed_count == encoder->typed_capacity) { + size_t capacity = scr_message_grow(encoder->typed_capacity); + encoder->typed = scr_message_regrow(encoder->typed, encoder->typed_count, capacity, + sizeof(*encoder->typed), NULL); + encoder->typed_capacity = capacity; + } node->kind = SCR_MESSAGE_TYPED_REF; - node->typed.ptr = input->v.typed_ref.ptr; - node->typed.retain = input->v.typed_ref.retain; - node->typed.release = input->v.typed_ref.release; - node->typed.type_key = input->v.typed_ref.type_key; - node->typed.type_key_len = input->v.typed_ref.type_key_len; - node->typed.materialize = input->v.typed_ref.materialize; - node->typed.commit = input->v.typed_ref.commit; - node->typed.traced = input->v.typed_ref.traced; + node->v.typed_index = encoder->typed_count; + encoder->typed[encoder->typed_count++] = (ScrMessageTyped){ + input->v.typed_ref.ptr, input->v.typed_ref.retain, input->v.typed_ref.release, + input->v.typed_ref.type_key, input->v.typed_ref.type_key_len, input->v.typed_ref.materialize, + input->v.typed_ref.commit, input->v.typed_ref.traced, + }; if (getenv("SCRIPTC_PUBLISH_TRACE")) - fprintf(stderr, "scriptc: published %.*s shared by reference\n", (int)node->typed.type_key_len, - node->typed.type_key); + fprintf(stderr, "scriptc: published %.*s shared by reference\n", (int)input->v.typed_ref.type_key_len, + input->v.typed_ref.type_key); return id; } if ((input->kind == SCR_DYN_OBJ || input->kind == SCR_DYN_ARR) && input->rc == SIZE_MAX) { scr_sa_thread_shares(); encoder->visits[id].view = (ScrDyn *)input; - ScrMessageNode *node = &encoder->message->nodes[id]; + ScrMessageNode *node = &encoder->nodes[id]; node->kind = SCR_MESSAGE_PUBLISHED; - node->published = (ScrDyn *)input; + node->v.published = (ScrDyn *)input; return id; } ScrDyn *value = input->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(input) : (ScrDyn *)input; encoder->visits[id].view = value; if (!value || scr_exc_pending()) return SIZE_MAX; - ScrMessageNode *node = &encoder->message->nodes[id]; + ScrMessageNode *node = &encoder->nodes[id]; ScrError *error = scr_errdyn_err_of(input); if (!error && value != input) error = scr_errdyn_err_of(value); if (error && uncaught && error->vt != &scr_error_vts[SCR_ERR_DOMEX]) { node->kind = SCR_MESSAGE_ERROR; - node->number = scr_message_error_kind(error); + node->v.number = scr_message_error_kind(error); scr_error_release(error); encoder->visits[id].uncaught = true; encoder->visits[id].keys = scr_message_uncaught_keys(value); @@ -327,7 +416,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) } if (error) { node->kind = SCR_MESSAGE_ERROR; - node->number = SCR_ERR_ERROR; + node->v.number = SCR_ERR_ERROR; static const char *const names[] = { "Error", "TypeError", "RangeError", "SyntaxError", "ReferenceError", "EvalError", "URIError", }; @@ -336,7 +425,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) }; for (size_t i = 0; i < sizeof names / sizeof names[0]; i++) if (error->name->len == strlen(names[i]) && !memcmp(error->name->data, names[i], error->name->len)) - node->number = kinds[i]; + node->v.number = kinds[i]; ScrDyn *keys = scr_dyn_new_arr(); static const char *const fields[] = {"message", "stack", "cause"}; for (size_t i = 0; i < sizeof fields / sizeof fields[0]; i++) { @@ -353,26 +442,28 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) switch (value->kind) { case SCR_DYN_UNDEF: node->kind = SCR_MESSAGE_UNDEFINED; break; case SCR_DYN_NULL: node->kind = SCR_MESSAGE_NULL; break; - case SCR_DYN_BOOL: node->kind = SCR_MESSAGE_BOOL; node->number = value->v.b; break; - case SCR_DYN_NUM: node->kind = SCR_MESSAGE_NUMBER; node->number = value->v.num; break; + case SCR_DYN_BOOL: node->kind = SCR_MESSAGE_BOOL; node->v.number = value->v.b; break; + case SCR_DYN_NUM: node->kind = SCR_MESSAGE_NUMBER; node->v.number = value->v.num; break; case SCR_DYN_STR: + /* The text is copied into the message block when it is packed. */ node->kind = SCR_MESSAGE_STRING; - node->length = value->v.str->len; - node->data = scr_message_copy(value->v.str->data, node->length); + encoder->visits[id].text = scr_str_retain(value->v.str); + node->v.text.length = value->v.str->len; + scr_message_text(encoder, node->v.text.length); break; case SCR_DYN_BIGINT: { ScrStr *text = scr_bigint_to_string(value->v.bigint, 10); if (!text) return SIZE_MAX; node->kind = SCR_MESSAGE_BIGINT; - node->length = text->len; - node->data = scr_message_copy(text->data, text->len); - scr_str_release(text); + encoder->visits[id].text = text; + node->v.text.length = text->len; + scr_message_text(encoder, node->v.text.length); break; } case SCR_DYN_ARR: case SCR_DYN_OBJ: node->kind = value->kind == SCR_DYN_ARR ? SCR_MESSAGE_ARRAY : SCR_MESSAGE_OBJECT; - node->length = value->kind == SCR_DYN_ARR ? value->v.arr.len : 0; + if (value->kind == SCR_DYN_ARR) node->v.length = value->v.arr.len; encoder->visits[id].keys = scr_dyn_own_keys(value); if (!encoder->visits[id].keys) return SIZE_MAX; scr_message_push(encoder, id); @@ -389,8 +480,9 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) return SIZE_MAX; } node->kind = SCR_MESSAGE_PORT; - node->channel = channel; - node->number = side; + node->v.port.channel = channel; + node->v.port.side = side; + encoder->releases = true; break; } bool buffer = scr_buffer_storage_is(value); @@ -402,21 +494,22 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) if (scr_exc_pending()) return SIZE_MAX; goto unsupported; } - node = &encoder->message->nodes[id]; + node = &encoder->nodes[id]; node->kind = SCR_MESSAGE_HANDLE; - node->transfer = transfer; - node->payload = payload; + node->v.handle.transfer = transfer; + node->v.handle.payload = payload; + encoder->releases = true; break; } const ScrBytes *bytes = buffer ? value->v.handle.ptr : value->v.bytes; size_t storage = scr_message_storage(encoder, bytes); /* Adding the storage node can reallocate the node array. */ - node = &encoder->message->nodes[id]; + node = &encoder->nodes[id]; node->kind = buffer ? SCR_MESSAGE_ARRAY_BUFFER : SCR_MESSAGE_BYTES; - node->storage = storage; - node->element = bytes->elem; - node->length = bytes->len; - node->offset = (size_t)(bytes->data - (bytes->backing ? bytes->backing : bytes)->data); + node->v.view.storage = storage; + node->element = (unsigned char)bytes->elem; + node->v.view.length = bytes->len; + node->v.view.offset = (size_t)(bytes->data - (bytes->backing ? bytes->backing : bytes)->data); node->data_view = bytes->is_data_view; break; } @@ -430,21 +523,20 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) static void scr_message_property(ScrMessageEncoder *encoder, size_t id, const ScrStr *key, size_t value, bool hidden) { - ScrMessageNode *node = &encoder->message->nodes[id]; - if (node->count == node->capacity) { - size_t capacity = scr_message_grow(node->capacity); - ScrMessageProperty *properties = scr_message_alloc(capacity, sizeof(*properties)); - if (node->count) memcpy(properties, node->properties, node->count * sizeof(*properties)); - free(node->properties); - node->properties = properties; - node->capacity = capacity; - } - node->properties[node->count++] = (ScrMessageProperty){ - (char *)scr_message_copy(key->data, key->len), key->len, value, hidden, - }; + if (encoder->property_count == encoder->property_capacity) { + size_t capacity = scr_message_grow(encoder->property_capacity); + encoder->properties = scr_message_regrow(encoder->properties, encoder->property_count, capacity, + sizeof(*encoder->properties), encoder->inline_properties); + encoder->property_capacity = capacity; + } + ScrMessageNode *node = &encoder->nodes[id]; + if (node->count == UINT32_MAX) scr_trap("scriptc: message too large\n"); + node->count++; + scr_message_text(encoder, key->len); + /* The key stays alive in the owner's key list until the message is packed. */ + encoder->properties[encoder->property_count++] = (ScrMessagePending){{key->data, key->len, value, hidden}, id}; } - /* ── publication: zero-copy delivery of deeply frozen plain-data graphs ── * Structured clone of a graph that is immutable on BOTH sides is observably * a share, as long as the receiver freezes what it receives (Node's clone is @@ -558,7 +650,8 @@ static ScrMessage *scr_message_try_publish(const ScrDyn *value) { if (!scr_publish_enabled || (value->kind != SCR_DYN_OBJ && value->kind != SCR_DYN_ARR)) return NULL; ScrMessage *message = NULL; ScrPublish p = {0}; - if (value->rc == SIZE_MAX || scr_publish_collect(&p, (ScrDyn *)value)) { + /* Most messages are not frozen: check the root before collecting. */ + if (value->rc == SIZE_MAX || (scr_publish_container_ok(value) && scr_publish_collect(&p, (ScrDyn *)value))) { for (size_t i = 0; i < p.count; i++) { ScrDyn *d = p.nodes[i]; if ((uintptr_t)d & 1) { @@ -582,11 +675,11 @@ static ScrMessage *scr_message_try_publish(const ScrDyn *value) { } } scr_sa_thread_shares(); - message = scr_message_alloc(1, sizeof(*message)); - message->nodes = scr_message_alloc(1, sizeof(*message->nodes)); - message->count = message->capacity = 1; + message = scr_message_alloc(1, sizeof(*message) + sizeof(*message->nodes)); + message->nodes = (ScrMessageNode *)(message + 1); + message->count = 1; message->nodes[0].kind = SCR_MESSAGE_PUBLISHED; - message->nodes[0].published = (ScrDyn *)value; + message->nodes[0].v.published = (ScrDyn *)value; if (getenv("SCRIPTC_PUBLISH_TRACE")) fprintf(stderr, "scriptc: published %zu nodes zero-copy%s\n", p.count, value->rc == SIZE_MAX && !p.count ? " (already published)" : ""); @@ -599,13 +692,14 @@ static ScrMessage *scr_message_try_publish(const ScrDyn *value) { } static ScrMessage *scr_message_encode_with(const ScrDyn *value, const ScrDyn *transfer); -static ScrMessage *scr_message_encode_run(ScrMessageEncoder encoder, const ScrDyn *value); +static ScrMessage *scr_message_encode_run(ScrMessageEncoder *encoder, const ScrDyn *value); ScrMessage *scr_message_encode(const ScrDyn *value) { return scr_message_encode_with(value, NULL); } ScrMessage *scr_message_encode_uncaught(const ScrDyn *value) { - ScrMessage *message = scr_message_alloc(1, sizeof(*message)); - return scr_message_encode_run((ScrMessageEncoder){.message = message, .uncaught = true}, value); + ScrMessageEncoder encoder; + scr_message_encoder_init(&encoder, NULL, true); + return scr_message_encode_run(&encoder, value); } ScrMessage *scr_message_encode_transfer(const ScrDyn *value, const ScrDyn *transfer) { @@ -635,19 +729,87 @@ ScrMessage *scr_message_encode_transfer(const ScrDyn *value, const ScrDyn *trans static ScrMessage *scr_message_encode_with(const ScrDyn *value, const ScrDyn *transfer) { ScrMessage *published = transfer ? NULL : scr_message_try_publish(value); if (published) return published; - ScrMessage *message = scr_message_alloc(1, sizeof(*message)); - return scr_message_encode_run((ScrMessageEncoder){.message = message, .transfer = transfer}, value); + ScrMessageEncoder encoder; + scr_message_encoder_init(&encoder, transfer, false); + return scr_message_encode_run(&encoder, value); +} + +/* Releases what a node owns: a STORAGE node's bytes, a PORT node's + * channel end and a HANDLE node's payload. */ +static void scr_message_drop(ScrMessageNode *node) { + switch (node->kind) { + case SCR_MESSAGE_STORAGE: + free(node->v.text.data); + scr_shared_release(node->v.text.shared); + break; + case SCR_MESSAGE_PORT: scr_message_port_ops->release(node->v.port.channel); break; + case SCR_MESSAGE_HANDLE: node->v.handle.transfer->drop(node->v.handle.payload); break; + default: break; + } +} + +/* Copies the encoded nodes into one block: the header, the nodes, the + * properties grouped by node (in the order they were reached), the typed + * records and the text. */ +static ScrMessage *scr_message_pack(ScrMessageEncoder *encoder) { + size_t count = encoder->count; + size_t properties = encoder->property_count; + size_t typed = encoder->typed_count; + size_t size = sizeof(ScrMessage); + size_t sizes[] = { + count, sizeof(ScrMessageNode), properties, sizeof(ScrMessageProperty), typed, sizeof(ScrMessageTyped), + }; + for (size_t i = 0; i < sizeof sizes / sizeof sizes[0]; i += 2) { + if (sizes[i] > (SIZE_MAX - size) / sizes[i + 1]) scr_trap("scriptc: message too large\n"); + size += sizes[i] * sizes[i + 1]; + } + if (encoder->text > SIZE_MAX - size) scr_trap("scriptc: message too large\n"); + ScrMessage *message = malloc(size + encoder->text); + if (!message) scr_trap("scriptc: out of memory\n"); + ScrMessageNode *nodes = (ScrMessageNode *)(message + 1); + ScrMessageProperty *slots = (ScrMessageProperty *)(nodes + count); + ScrMessageTyped *records = (ScrMessageTyped *)(slots + properties); + char *text = (char *)(records + typed); + *message = (ScrMessage){nodes, count, encoder->storage, encoder->releases}; + memcpy(nodes, encoder->nodes, count * sizeof(*nodes)); + if (typed) memcpy(records, encoder->typed, typed * sizeof(*records)); + size_t first = 0; + for (size_t i = 0; i < count; i++) { + ScrMessageNode *node = &nodes[i]; + node->properties = slots + first; + first += node->count; + node->count = 0; + if (node->kind == SCR_MESSAGE_STRING || node->kind == SCR_MESSAGE_BIGINT) { + memcpy(text, encoder->visits[i].text->data, node->v.text.length); + text[node->v.text.length] = 0; + node->v.text.data = text; + text += node->v.text.length + 1; + } else if (node->kind == SCR_MESSAGE_TYPED_REF) { + node->v.typed = &records[node->v.typed_index]; + } + } + for (size_t i = 0; i < properties; i++) { + const ScrMessagePending *pending = &encoder->properties[i]; + ScrMessageNode *node = &nodes[pending->owner]; + ScrMessageProperty *slot = (ScrMessageProperty *)&node->properties[node->count++]; + *slot = pending->property; + memcpy(text, pending->property.key, pending->property.length); + text[pending->property.length] = 0; + slot->key = text; + text += pending->property.length + 1; + } + return message; } -static ScrMessage *scr_message_encode_run(ScrMessageEncoder encoder, const ScrDyn *value) { - ScrMessage *message = encoder.message; - if (scr_message_value(&encoder, value) == SIZE_MAX) goto done; - while (encoder.depth && !scr_exc_pending()) { - size_t id = encoder.stack[encoder.depth - 1]; - ScrMessageVisit *visit = &encoder.visits[id]; - if (visit->cursor == visit->keys->v.arr.len) { encoder.depth--; continue; } +static ScrMessage *scr_message_encode_run(ScrMessageEncoder *encoder, const ScrDyn *value) { + ScrMessage *message = NULL; + if (scr_message_value(encoder, value) == SIZE_MAX) goto done; + while (encoder->depth && !scr_exc_pending()) { + size_t id = encoder->stack[encoder->depth - 1]; + ScrMessageVisit *visit = &encoder->visits[id]; + if (visit->cursor == visit->keys->v.arr.len) { encoder->depth--; continue; } ScrDyn *key = visit->keys->v.arr.items[visit->cursor++]; - bool error = message->nodes[id].kind == SCR_MESSAGE_ERROR; + bool error = encoder->nodes[id].kind == SCR_MESSAGE_ERROR; if (key->kind != SCR_DYN_STR || (!error && !scr_dyn_property_is_enumerable(visit->view, key->v.str))) continue; ScrDyn *child = scr_dyn_bag_get(visit->view, key->v.str, visit->view); if (!child || scr_exc_pending()) { scr_dyn_release(child); break; } @@ -657,33 +819,30 @@ static ScrMessage *scr_message_encode_run(ScrMessageEncoder encoder, const ScrDy * and symbols, and serializes its cause the same way. */ if (child->kind == SCR_DYN_FUNC || child->kind == SCR_DYN_SYMBOL) { scr_dyn_release(child); continue; } hidden = !scr_dyn_has_own(visit->view, key->v.str) || !scr_dyn_property_is_enumerable(visit->view, key->v.str); - encoder.uncaught = key->v.str->len == 5 && !memcmp(key->v.str->data, "cause", 5); + encoder->uncaught = key->v.str->len == 5 && !memcmp(key->v.str->data, "cause", 5); } - size_t next = scr_message_value(&encoder, child); + size_t next = scr_message_value(encoder, child); scr_dyn_release(child); if (next == SIZE_MAX) break; - scr_message_property(&encoder, id, key->v.str, next, hidden); + scr_message_property(encoder, id, key->v.str, next, hidden); } done: - for (size_t i = 0; i < message->count; i++) { - ScrMessageVisit *visit = &encoder.visits[i]; + if (!scr_exc_pending()) message = scr_message_pack(encoder); + else + for (size_t i = 0; i < encoder->count; i++) scr_message_drop(&encoder->nodes[i]); + for (size_t i = 0; i < encoder->count; i++) { + ScrMessageVisit *visit = &encoder->visits[i]; scr_dyn_release(visit->keys); if (visit->view != visit->input) scr_dyn_release(visit->view); scr_dyn_release((ScrDyn *)visit->input); - } - free(encoder.visits); - free(encoder.identities); - free(encoder.stack); - if (scr_exc_pending()) { scr_message_free(message); return NULL; } - /* A queued message keeps its nodes until it is received: drop the - * growth slack. */ - if (message->count && message->count < message->capacity) { - ScrMessageNode *nodes = realloc(message->nodes, message->count * sizeof(*nodes)); - if (nodes) { - message->nodes = nodes; - message->capacity = message->count; - } - } + scr_str_release(visit->text); + } + if (encoder->nodes != encoder->inline_nodes) free(encoder->nodes); + if (encoder->visits != encoder->inline_visits) free(encoder->visits); + if (encoder->properties != encoder->inline_properties) free(encoder->properties); + if (encoder->identities != encoder->inline_identities) free(encoder->identities); + if (encoder->stack != encoder->inline_stack) free(encoder->stack); + free(encoder->typed); return message; } @@ -732,26 +891,31 @@ static void scr_message_tag_document(const ScrMessage *message, ScrDyn **values) ScrDyn *scr_message_decode(const ScrMessage *message) { if (message->count == 1 && message->nodes[0].kind == SCR_MESSAGE_PUBLISHED) - return scr_dyn_retain(message->nodes[0].published); - ScrDyn **values = scr_message_alloc(message->count, sizeof(*values)); - ScrBytes **storage = scr_message_alloc(message->count, sizeof(*storage)); - for (size_t i = 0; i < message->count; i++) { + return scr_dyn_retain(message->nodes[0].v.published); + ScrDyn *inline_values[SCR_MESSAGE_INLINE]; + ScrDyn **values = message->count <= SCR_MESSAGE_INLINE + ? inline_values : scr_message_alloc(message->count, sizeof(*values)); + memset(values, 0, message->count * sizeof(*values)); + ScrBytes **storage = message->storage ? scr_message_alloc(message->count, sizeof(*storage)) : NULL; + for (size_t i = 0; storage && i < message->count; i++) { const ScrMessageNode *node = &message->nodes[i]; if (node->kind != SCR_MESSAGE_STORAGE) continue; - storage[i] = node->shared ? scr_shared_wrap(node->shared) : scr_bytes_new(SCR_BYTES_U8, (double)node->length); + storage[i] = node->v.text.shared ? scr_shared_wrap(node->v.text.shared) + : scr_bytes_new(SCR_BYTES_U8, (double)node->v.text.length); if (!storage[i]) goto done; - if (!node->shared && node->length) memcpy(storage[i]->data, node->data, node->length); + if (!node->v.text.shared && node->v.text.length) + memcpy(storage[i]->data, node->v.text.data, node->v.text.length); } for (size_t i = 0; i < message->count; i++) { const ScrMessageNode *node = &message->nodes[i]; switch (node->kind) { case SCR_MESSAGE_UNDEFINED: values[i] = scr_dyn_retain(scr_dyn_undefined()); break; case SCR_MESSAGE_NULL: values[i] = scr_dyn_new_null(); break; - case SCR_MESSAGE_BOOL: values[i] = scr_dyn_new_bool(node->number != 0); break; - case SCR_MESSAGE_NUMBER: values[i] = scr_dyn_new_num(node->number); break; + case SCR_MESSAGE_BOOL: values[i] = scr_dyn_new_bool(node->v.number != 0); break; + case SCR_MESSAGE_NUMBER: values[i] = scr_dyn_new_num(node->v.number); break; case SCR_MESSAGE_STRING: case SCR_MESSAGE_BIGINT: { - ScrStr *text = scr_str_new((const char *)node->data, node->length); + ScrStr *text = scr_str_new(node->v.text.data, node->v.text.length); if (node->kind == SCR_MESSAGE_STRING) values[i] = scr_dyn_new_str(text); else { ScrBigInt *integer = scr_bigint_parse(text); @@ -762,19 +926,19 @@ ScrDyn *scr_message_decode(const ScrMessage *message) { } case SCR_MESSAGE_ARRAY: values[i] = scr_dyn_new_arr(); - for (size_t j = 0; j < node->length; j++) scr_dyn_arr_push_hole(values[i]); + for (size_t j = 0; j < node->v.length; j++) scr_dyn_arr_push_hole(values[i]); break; case SCR_MESSAGE_OBJECT: values[i] = scr_dyn_new_obj(); break; case SCR_MESSAGE_ERROR: { scr_error_set_traced(); - ScrError *error = scr_error_new((int)node->number, NULL); + ScrError *error = scr_error_new((int)node->v.number, NULL); values[i] = scr_dyn_from_error(error); scr_error_release(error); break; } case SCR_MESSAGE_BYTES: { - ScrBytes *bytes = scr_bytes_buffer_view(storage[node->storage], node->element, - (double)node->offset, true, (double)node->length); + ScrBytes *bytes = scr_bytes_buffer_view(storage[node->v.view.storage], (ScrBytesElem)node->element, + (double)node->v.view.offset, true, (double)node->v.view.length); if (bytes) { bytes->is_data_view = node->data_view; values[i] = scr_dyn_new_bytes(bytes); @@ -783,19 +947,21 @@ ScrDyn *scr_message_decode(const ScrMessage *message) { break; } case SCR_MESSAGE_ARRAY_BUFFER: - values[i] = scr_array_buffer_from_bytes(storage[node->storage]); + values[i] = scr_array_buffer_from_bytes(storage[node->v.view.storage]); break; case SCR_MESSAGE_STORAGE: continue; - case SCR_MESSAGE_PUBLISHED: values[i] = scr_dyn_retain(node->published); break; - case SCR_MESSAGE_TYPED_REF: - values[i] = (node->typed.traced ? scr_dyn_new_typed_ref_traced : scr_dyn_new_typed_ref)( - node->typed.ptr, node->typed.retain, node->typed.release, node->typed.type_key, - node->typed.type_key_len, node->typed.materialize, node->typed.commit); + case SCR_MESSAGE_PUBLISHED: values[i] = scr_dyn_retain(node->v.published); break; + case SCR_MESSAGE_TYPED_REF: { + const ScrMessageTyped *typed = node->v.typed; + values[i] = (typed->traced ? scr_dyn_new_typed_ref_traced : scr_dyn_new_typed_ref)( + typed->ptr, typed->retain, typed->release, typed->type_key, typed->type_key_len, + typed->materialize, typed->commit); break; + } case SCR_MESSAGE_PORT: - values[i] = scr_message_port_ops->adopt(node->channel, (int)node->number); + values[i] = scr_message_port_ops->adopt(node->v.port.channel, node->v.port.side); break; - case SCR_MESSAGE_HANDLE: values[i] = node->transfer->adopt(node->payload); break; + case SCR_MESSAGE_HANDLE: values[i] = node->v.handle.transfer->adopt(node->v.handle.payload); break; } if (!values[i] || scr_exc_pending()) goto done; } @@ -805,7 +971,7 @@ ScrDyn *scr_message_decode(const ScrMessage *message) { const ScrMessageProperty *property = &node->properties[j]; ScrDyn *value = scr_dyn_retain(values[property->value]); if (node->kind == SCR_MESSAGE_ARRAY) { - size_t index = scr_message_array_index(property->key, property->length, node->length); + size_t index = scr_message_array_index(property->key, property->length, node->v.length); if (index != SIZE_MAX) { values[i]->v.arr.items[index] = value; values[i]->v.arr.presence[index] = 1; @@ -841,24 +1007,16 @@ done:; ScrDyn *result = !scr_exc_pending() ? scr_dyn_retain(values[0]) : NULL; for (size_t i = 0; i < message->count; i++) { scr_dyn_release(values[i]); - scr_bytes_release(storage[i]); + if (storage) scr_bytes_release(storage[i]); } - free(values); + if (values != inline_values) free(values); free(storage); return result; } void scr_message_free(ScrMessage *message) { if (!message) return; - for (size_t i = 0; i < message->count; i++) { - ScrMessageNode *node = &message->nodes[i]; - for (size_t j = 0; j < node->count; j++) free(node->properties[j].key); - free(node->properties); - free(node->data); - scr_shared_release(node->shared); - if (node->kind == SCR_MESSAGE_PORT) scr_message_port_ops->release(node->channel); - if (node->kind == SCR_MESSAGE_HANDLE) node->transfer->drop(node->payload); - } - free(message->nodes); + if (message->releases) + for (size_t i = 0; i < message->count; i++) scr_message_drop(&message->nodes[i]); free(message); } diff --git a/packages/runtime/src/scr_message.test.c b/packages/runtime/src/scr_message.test.c index a6c940783f..bdabc4d12d 100644 --- a/packages/runtime/src/scr_message.test.c +++ b/packages/runtime/src/scr_message.test.c @@ -172,6 +172,84 @@ static void check_documents(void) { scr_message_free(message); } +/* A message larger than the encoder's inline arrays (nodes, properties, + * identities and the visit stack) keeps its key order, shared values and + * text, and an encode started from a getter during another encode + * produces its own message. */ +static SCR_TL ScrMessage *nested_message; +static ScrDyn *nested_getter(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + ScrDyn *inner = scr_dyn_new_arr(); + scr_dyn_arr_push(inner, scr_dyn_new_num(41)); + nested_message = scr_message_encode(inner); + scr_dyn_release(inner); + return scr_dyn_new_num(42); +} + +static void check_large(void) { + ScrDyn *root = scr_dyn_new_obj(); + ScrDyn *shared = scr_dyn_new_obj(); + put_number(shared, "shared", 1); + ScrDyn *chain = root; + char key[32]; + for (int i = 0; i < 40; i++) { + ScrDyn *item = scr_dyn_new_obj(); + put_number(item, "index", i); + snprintf(key, sizeof key, "label-%d", i); + ScrStr *text = scr_str_new(key, strlen(key)); + scr_dyn_obj_set(item, "label", 5, scr_dyn_new_str(text)); + scr_str_release(text); + scr_dyn_obj_set(item, "shared", 6, scr_dyn_retain(shared)); + snprintf(key, sizeof key, "item%d", i); + scr_dyn_obj_set(root, key, strlen(key), item); + /* A chain 40 objects deep grows the visit stack. */ + ScrDyn *next = scr_dyn_new_obj(); + scr_dyn_obj_set(chain, "next", 4, next); + chain = next; + } + scr_dyn_release(shared); + ScrClosure *closure = scr_closure_new(NULL, 0); + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "get", 3, scr_dyn_new_func(closure, nested_getter, 0, "", "")); + scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(true)); + ScrStr *name = scr_str_new("nested", 6); + ScrDyn *name_value = scr_dyn_new_str(name); + scr_dyn_release(scr_dyn_define_property(root, name_value, descriptor)); + scr_dyn_release(name_value); + scr_dyn_release(descriptor); + scr_str_release(name); + ScrMessage *message = scr_message_encode(root); + assert(message && !scr_exc_pending() && nested_message); + scr_dyn_release(root); + scr_collect_cycles(); + ScrDyn *decoded = scr_message_decode(message); + assert(decoded && !scr_exc_pending()); + assert(decoded->v.obj.len == 42); + assert(decoded->v.obj.entries[0].key_len == 5 && !memcmp(decoded->v.obj.entries[0].key, "item0", 5)); + assert(decoded->v.obj.entries[41].key_len == 6 && !memcmp(decoded->v.obj.entries[41].key, "nested", 6)); + assert(scr_dyn_obj_get(decoded, "nested", 6)->v.num == 42); + ScrDyn *first = scr_dyn_obj_get(decoded, "item0", 5); + for (int i = 0; i < 40; i++) { + snprintf(key, sizeof key, "item%d", i); + ScrDyn *item = scr_dyn_obj_get(decoded, key, strlen(key)); + assert(item && scr_dyn_obj_get(item, "index", 5)->v.num == i); + assert(scr_dyn_obj_get(item, "shared", 6) == scr_dyn_obj_get(first, "shared", 6)); + snprintf(key, sizeof key, "label-%d", i); + ScrStr *label = scr_dyn_obj_get(item, "label", 5)->v.str; + assert(label->len == strlen(key) && !memcmp(label->data, key, label->len)); + } + ScrDyn *link = decoded; + for (int i = 0; i < 40; i++) link = scr_dyn_obj_get(link, "next", 4); + assert(link && link->kind == SCR_DYN_OBJ && link->v.obj.len == 0); + scr_dyn_release(decoded); + ScrDyn *inner = scr_message_decode(nested_message); + assert(inner && inner->v.arr.len == 1 && inner->v.arr.items[0]->v.num == 41); + scr_dyn_release(inner); + scr_collect_cycles(); + scr_message_free(nested_message); + scr_message_free(message); +} + /* A link-gated unit's handle crosses threads through the transfer hooks in * its ops: share on the sender, adopt on the receiver, drop with the * message. The test borrows the last capacity slot, which no unit uses, so @@ -297,6 +375,7 @@ int main(void) { check_getters(); check_rejection(); check_documents(); + check_large(); check_handle_transfer(); scr_context_cleanup(); puts("message graphs preserve identity, sparse values, byte aliases and getter order"); From 1a51223cd6f4b4899761a7ec8da3378e0f048c68 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 01:01:01 -0500 Subject: [PATCH 067/395] Say where the region's trap and memoryUsage accounting come from Comments only: scr_trap is the runtime's failure path and the standalone allocator test defines its own, and the region's chunks count in process.memoryUsage() through scr_heap_stats. --- packages/runtime/src/scr_alloc.c | 4 ++++ packages/runtime/src/scr_lib.h | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index f2304aacef..3a36f672ef 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -2072,6 +2072,8 @@ static __attribute__((noinline)) void scr_plr_reserve(void) { } } __atomic_clear(&lock, __ATOMIC_RELEASE); + /* scr_trap is the runtime's failure path (scr_exception.c); the + * allocator's standalone test defines its own (scr_alloc.test.c). */ if (!scr_plr_base) { static const char message[] = "scriptc: cannot reserve address space below 32 GiB for program-lifetime objects\n"; @@ -2188,6 +2190,8 @@ static __attribute__((noinline)) void *scr_plr_refill(size_t n) { scr_plr_mine_waste += left; #endif } + /* process.memoryUsage() counts the chunk in heapTotal and heapUsed + * (scr_heap_stats, scr_plr_heap_stats). */ __atomic_fetch_add(&scr_plr_held, chunk, __ATOMIC_RELAXED); scr_plr_chunk.next = p + SCR_PLR_LINK + n; scr_plr_chunk.limit = p + chunk; diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index 1b859cea11..411d1e684b 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -238,7 +238,8 @@ double scr_process_memory_usage_field(double k); double scr_process_rss(void); /* The runtime heap's footprint (scr_alloc.c): bytes held from the system * and bytes in live allocations, for the calling thread's allocator plus - * the system allocator where it reports them. */ + * the system allocator where it reports them, plus the program-lifetime + * region's chunks (compact class instances). */ void scr_heap_stats(double *total, double *used); /* process.ppid: the parent's pid at the time of the read. */ double scr_process_ppid(void); From 93e13f49a0d0e44896769ec157bd24b153b73492 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 01:06:31 -0500 Subject: [PATCH 068/395] Keep structural names apart by declaration kind and name recursive types by position Structural positions named an enclosing declaration by its name and its ordinal among the declarations of the same kind and name, without the kind. A getter and the setter of the same property, or a class and the namespace merged with it, therefore had the same path, and local classes declared in both shared one class. The path now includes the kind, and interfaces and type aliases anchor the positions inside them. Recursive record and union placeholders were named by their printed type and the byte offset of their declaration. A comment or blank line above the type renamed them, and two instantiations that print alike (type arguments that are type parameters or classes of one name from different declarations) threw an InternalCompilerError. Placeholders are now named by the printed type and the structural position of the declaration, and the same for up to three levels of type arguments. A description that two types still share gets a numbered suffix for the later one. --- .../src/frontend/lowering/lowerer.test.ts | 16 +-- .../compiler/src/frontend/lowering/lowerer.ts | 1 + .../frontend/lowering/structural-positions.ts | 16 ++- .../compiler/src/frontend/type-mapper.test.ts | 2 +- packages/compiler/src/frontend/type-mapper.ts | 78 +++++++++--- packages/compiler/test/stable-names.test.ts | 112 ++++++++++++++++++ 6 files changed, 192 insertions(+), 33 deletions(-) create mode 100644 packages/compiler/test/stable-names.test.ts diff --git a/packages/compiler/src/frontend/lowering/lowerer.test.ts b/packages/compiler/src/frontend/lowering/lowerer.test.ts index 750c27694c..7fbb9661fc 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.test.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.test.ts @@ -33,7 +33,7 @@ test("reuses complete copy routes without repeating discriminator planning", () expect(plan).not.toHaveBeenCalled(); expect(lowerer.liftedFns.filter((fn) => fn.name === helper)).toHaveLength(1); const recursive = {} as Type; - lowerer.shapes.recursiveRef(recursive, "Recursive"); + lowerer.shapes.recursiveRef(recursive, () => "Recursive"); lowerer.shapes.finalizeRecursive(recursive, [{ name: "kind", type: STRING }]); expect(lowerer.unionRetagHelper(from, to, loc)).toBe(helper); expect(plan).toHaveBeenCalled(); @@ -55,7 +55,7 @@ test("replans after a recursive union gains its final arms", () => { const lowerer = context(); const recursive = {} as Type; const from = lowerer.unions.intern([UNDEFINED_T, NULL_T]); - const to = lowerer.unions.recursiveRef(recursive, "Recursive"); + const to = lowerer.unions.recursiveRef(recursive, () => "Recursive"); const trapped = lowerer.unionRetagHelper(from, to, loc); expect(trapped).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [UNDEFINED_T]); @@ -76,7 +76,7 @@ test("rechecks width conversions after recursive record definitions settle", () const lowerer = context(); const recursive = {} as Type; const source = record(lowerer.shapes.intern([{ name: "value", type: F64 }])); - const target = record(lowerer.shapes.recursiveRef(recursive, "Recursive")); + const target = record(lowerer.shapes.recursiveRef(recursive, () => "Recursive")); const from = lowerer.unions.intern([source, UNDEFINED_T]); const to = lowerer.unions.intern([target, NULL_T]); expect(lowerer.unionRetagHelper(from, to, loc)).not.toBeNull(); @@ -130,7 +130,7 @@ test("literal ownership is reused across values and rechecked when recursive rec expect(inspect).not.toHaveBeenCalled(); const recursive = {} as Type; - const pendingShape = record(lowerer.shapes.recursiveRef(recursive, "Recursive")); + const pendingShape = record(lowerer.shapes.recursiveRef(recursive, () => "Recursive")); const pending = lowerer.unions.intern([pendingShape], { field: "kind", cases: [{ tag: 0, values: ["a"] }], @@ -144,7 +144,7 @@ test("literal ownership is reused across values and rechecked when recursive rec test("literal ownership sees a recursive union's finalized discriminator", () => { const lowerer = context(); const recursive = {} as Type; - const union = lowerer.unions.recursiveRef(recursive, "Recursive"); + const union = lowerer.unions.recursiveRef(recursive, () => "Recursive"); const shape = record(lowerer.shapes.intern([{ name: "kind", type: STRING }])); expect(lowerer.literalUnionArm(union, ["ready"])).toBeNull(); lowerer.unions.finalizeRecursive(recursive, [shape], { @@ -185,7 +185,7 @@ test("removing undefined reuses the discriminator transformation without hiding const recursive = {} as Type; const pending: IrType = { kind: "union", - unionId: lowerer.unions.recursiveRef(recursive, "Recursive"), + unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), }; expect(lowerer.stripUndefinedArm(pending)).toEqual(pending); lowerer.unions.finalizeRecursive(recursive, [STRING, UNDEFINED_T]); @@ -221,7 +221,7 @@ test("adding undefined reuses semantic union transformations and sees recursive const recursive = {} as Type; const pending: IrType = { kind: "union", - unionId: lowerer.unions.recursiveRef(recursive, "Recursive"), + unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), }; const before = lowerer.withUndefinedArmOf(pending); lowerer.unions.finalizeRecursive(recursive, [a, b], discriminant); @@ -236,7 +236,7 @@ test("optional union widening keeps refused and missing contracts current", () = const recursive = {} as Type; const pending: IrType = { kind: "union", - unionId: lowerer.unions.recursiveRef(recursive, "Recursive"), + unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), }; expect(lowerer.withUndefinedArmOf(pending)).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [{ kind: "date" }, STRING]); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 687a2fb1af..3f8a0a66e5 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3568,6 +3568,7 @@ export class Lowerer { this.checker = program.getTypeChecker(); this.typeCtx = { checker: this.checker, + positionOf: (node) => this.positions.of(node), shapes: this.shapes, unions: this.unions, classNamer: this.classNamer, diff --git a/packages/compiler/src/frontend/lowering/structural-positions.ts b/packages/compiler/src/frontend/lowering/structural-positions.ts index cdc290b4bf..ea8535d9ef 100644 --- a/packages/compiler/src/frontend/lowering/structural-positions.ts +++ b/packages/compiler/src/frontend/lowering/structural-positions.ts @@ -15,6 +15,8 @@ const ANCHOR_KINDS: ReadonlySet = new Set([ ts.SyntaxKind.ModuleDeclaration, ts.SyntaxKind.EnumDeclaration, ts.SyntaxKind.Constructor, + ts.SyntaxKind.InterfaceDeclaration, + ts.SyntaxKind.TypeAliasDeclaration, ]); /** The name of `node` when it is a named declaration that anchors the @@ -34,10 +36,10 @@ function anchorOf(node: ts.Node): ts.Node { /** Source positions that stay the same when code outside the enclosing * declaration changes. A node's structural position is its file, the chain - * of enclosing named declarations (each with its ordinal among the - * declarations of the same kind and name in its own anchor), and its - * ordinal among the nodes of its kind whose nearest anchor is the same - * declaration. Editing another declaration, or code nested in another + * of enclosing named declarations (each with its kind, its name and its + * ordinal among the declarations of the same kind and name in its own + * anchor), and its ordinal among the nodes of its kind whose nearest anchor + * is the same declaration. Editing another declaration, or code nested in another * named declaration of the same anchor, leaves the position unchanged. */ export class StructuralPositions { /** Per anchor and key: the indices of the nodes with that key whose @@ -75,8 +77,10 @@ export class StructuralPositions { if (ts.isSourceFile(anchor)) path = anchor.fileName; else { const parent = anchorOf(anchor); - const name = anchorName(anchor); - path = `${this.pathOf(parent)}/${name}#${this.ordinalIn(parent, anchor, `${anchor.kind}:${name}`)}`; + // The kind keeps declarations of one name apart: a getter and its + // setter, or a class and the namespace merged with it. + const key = `${anchor.kind}:${anchorName(anchor)}`; + path = `${this.pathOf(parent)}/${key}#${this.ordinalIn(parent, anchor, key)}`; } this.paths.set(anchor, path); return path; diff --git a/packages/compiler/src/frontend/type-mapper.test.ts b/packages/compiler/src/frontend/type-mapper.test.ts index a99eb44cc7..163ba7b9ee 100644 --- a/packages/compiler/src/frontend/type-mapper.test.ts +++ b/packages/compiler/src/frontend/type-mapper.test.ts @@ -41,7 +41,7 @@ describe("union arm lookup", () => { test("sees finalized recursive arms after a missing lookup", () => { const unions = new UnionRegistry(); const recursive = {} as Type; - const union = unions.recursiveRef(recursive, "Recursive"); + const union = unions.recursiveRef(recursive, () => "Recursive"); expect(unions.armTag(union, STRING)).toBe(-1); unions.finalizeRecursive(recursive, [ F64, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index ce73e8bc25..79091cadcc 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1,5 +1,5 @@ import { InternalCompilerError } from "../errors.js"; -import { keyedHelperName } from "./lowering/stable-names.js"; +import { keyedHelperName, type KeyedNameOwner } from "./lowering/stable-names.js"; import { mockCallRecordType, mockContextType } from "./mock-shapes.js"; import * as ts from "./ts7/adapter.js"; import { bodyReadsArguments } from "./arguments-usage.js"; @@ -393,13 +393,12 @@ export class ShapeRegistry { /** The shape id a back-reference to an in-progress type resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER - * entry (empty fields) the outer frame finalizes. */ - recursiveRef(t: ts.Type, description: string): string { + * entry (empty fields) the outer frame finalizes. `describe` names a new + * placeholder (recursiveTypeName). */ + recursiveRef(t: ts.Type, describe: () => string): string { let id = this.recIds.get(t); if (id === undefined) { - id = keyedHelperName(this, "r", `recursive:${description}`, ""); - if (this.byId.has(id)) - throw new InternalCompilerError(`two recursive types share the description ${description}`); + id = recursivePlaceholderName(this, "r", describe(), (name) => this.byId.has(name)); const shape: IrRecordShape = { id, fields: [] }; this.byId.set(id, shape); this.shapes.push(shape); @@ -546,15 +545,55 @@ export class ShapeRegistry { } } -/** A recursive checker type's printed form and first declaration, which - * name its placeholder whichever frame first reaches it. */ -function recursiveTypeName(t: ts.Type, ctx: Pick): string { +/** The id of a recursive placeholder described by `description`. Two + * different types can still share a description (recursiveTypeName stops + * after a few levels of type arguments); the later one takes the first free + * name with a numbered suffix. */ +function recursivePlaceholderName( + owner: KeyedNameOwner, + prefix: string, + description: string, + taken: (name: string) => boolean, +): string { + let name = keyedHelperName(owner, prefix, `recursive:${description}`, ""); + for (let n = 1; taken(name); n++) + name = keyedHelperName(owner, prefix, `recursive:${description}#${n}`, ""); + return name; +} + +/** How many levels of type arguments recursiveTypeName describes. */ +const RECURSIVE_NAME_DEPTH = 3; + +/** A recursive checker type's description, which names its placeholder + * whichever frame first reaches it: its printed form, the structural + * position of its first declaration, and the same for its type arguments + * (two instantiations print the same when their type arguments are type + * parameters or classes of the same name from different declarations). */ +function recursiveTypeName( + t: ts.Type, + ctx: Pick, + depth = 0, +): string { const symbol = t.getAliasSymbol() ?? t.getSymbol(); const declaration = symbol?.declarations[0]; const where = declaration - ? `${ts.getSourceFileOfNode(declaration).fileName}:${declaration.pos}` + ? (ctx.positionOf?.(declaration) ?? + `${ts.getSourceFileOfNode(declaration).fileName}:${declaration.pos}`) : ""; - return `${ctx.checker.typeToString(t)}@${where}`; + let description = `${ctx.checker.typeToString(t)}@${where}`; + if (depth < RECURSIVE_NAME_DEPTH) { + const aliasArguments = + t.getAliasSymbol() !== undefined ? (t.getAliasTypeArguments() ?? []) : []; + const typeArguments = + aliasArguments.length > 0 + ? aliasArguments + : t.isTypeReference() + ? ctx.checker.getTypeArguments(t) + : []; + if (typeArguments.length > 0) + description += `<${typeArguments.map((arg) => recursiveTypeName(arg, ctx, depth + 1)).join(",")}>`; + } + return description; } /** The frontend's union interner — mirrors ShapeRegistry. A union's @@ -585,13 +624,12 @@ export class UnionRegistry { /** The union id a back-reference to an in-progress union resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER - * entry (empty arms) the outer frame finalizes. */ - recursiveRef(t: ts.Type, description: string): string { + * entry (empty arms) the outer frame finalizes. `describe` names a new + * placeholder (recursiveTypeName). */ + recursiveRef(t: ts.Type, describe: () => string): string { let id = this.recIds.get(t); if (id === undefined) { - id = keyedHelperName(this, "u", `recursive:${description}`, ""); - if (this.byId.has(id)) - throw new InternalCompilerError(`two recursive types share the description ${description}`); + id = recursivePlaceholderName(this, "u", describe(), (name) => this.byId.has(name)); const def: IrUnionDef = { id, arms: [] }; this.byId.set(id, def); this.unions.push(def); @@ -1135,6 +1173,10 @@ export interface TypeMapperCtx { * empty object type rule in the object mapping). */ emptyLiteralArm?: boolean; checker: ts.TypeChecker; + /** The structural position of a declaration (structural-positions.ts), + * which names recursive placeholders independently of where unrelated + * code sits. The Lowerer supplies it. */ + positionOf?: (node: ts.Node) => string; /** JavaScript declarations whose instance or object-literal types only * the checked-dynamic representation holds (js-dynamic-objects.ts). * Absent when the program has none: every mapping is unchanged. */ @@ -4673,7 +4715,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (unions.inProgress.has(widened)) { return { kind: "union", - unionId: unions.recursiveRef(widened, recursiveTypeName(widened, ctx)), + unionId: unions.recursiveRef(widened, () => recursiveTypeName(widened, ctx)), }; } unions.inProgress.add(widened); @@ -7020,7 +7062,7 @@ function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { if (shapes.inProgress.has(widened)) { return { kind: "record", - shapeId: shapes.recursiveRef(widened, recursiveTypeName(widened, ctx)), + shapeId: shapes.recursiveRef(widened, () => recursiveTypeName(widened, ctx)), }; } shapes.inProgress.add(widened); diff --git a/packages/compiler/test/stable-names.test.ts b/packages/compiler/test/stable-names.test.ts new file mode 100644 index 0000000000..720472556d --- /dev/null +++ b/packages/compiler/test/stable-names.test.ts @@ -0,0 +1,112 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule, type IrModule } from "../src/index.js"; + +/** Lowers each source in turn as the same file, so names that include the + * file's path compare equal. */ +async function lower(...sources: string[]): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-stable-names-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + const modules: IrModule[] = []; + for (const source of sources) { + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + modules.push(deserializeModule(await readFile(outPath, "utf8"))); + } + return modules; + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function names(mod: IrModule): string[] { + return [ + ...mod.records.map((record) => record.id), + ...mod.unions.map((union) => union.id), + ...mod.functions.map((fn) => fn.name), + ...mod.classes.map((cls) => cls.name), + ].sort(); +} + +const recursive = ` +type Tree = { value: number; children: Tree[]; parent: Tree | null }; +function total(tree: Tree): number { + let sum = tree.value; + for (const child of tree.children) sum += total(child); + return sum; +} +const root: Tree = { value: 1, children: [], parent: null }; +root.children.push({ value: 2, children: [], parent: root }); +console.log(total(root)); +`; + +test("recursive record and union ids do not depend on the code above the type", async () => { + const [before, after] = await lower( + recursive, + `// A comment above the type.\n\ntype Unrelated = { name: string };\n\n${recursive}`, + ); + expect(before.records.some((record) => record.fields.some((f) => f.name === "children"))).toBe( + true, + ); + expect(names(after)).toEqual(names(before)); +}); + +test("recursive instantiations that print alike get distinct placeholders", async () => { + const [mod] = await lower(` +namespace A { + export class Item { + a = 1; + } +} +namespace B { + export class Item { + b = "b"; + } +} +type Chain = { item: T; next: Chain | null }; +const first: Chain = { item: new A.Item(), next: null }; +const second: Chain = { item: new B.Item(), next: null }; +first.next = { item: new A.Item(), next: null }; +second.next = { item: new B.Item(), next: null }; +console.log(first.next.item.a, second.next.item.b); +`); + const chains = mod.records.filter((record) => record.fields.some((f) => f.name === "next")); + expect(new Set(chains.map((record) => record.id)).size).toBe(chains.length); + expect(chains.length).toBeGreaterThanOrEqual(2); +}); + +test("a getter and a setter of one name keep their local classes apart", async () => { + const [mod] = await lower(` +class Box { + private v = 0; + get value(): number { + class Local { + n(): number { + return 1; + } + } + return new Local().n() + this.v; + } + set value(x: number) { + class Local { + n(): number { + return 2; + } + } + this.v = x * new Local().n(); + } +} +const box = new Box(); +box.value = 5; +console.log(box.value); +`); + const locals = mod.classes.filter((cls) => cls.name.includes("Local")); + expect(locals).toHaveLength(2); + expect(locals[0]!.name).not.toBe(locals[1]!.name); +}); From d68b73e3b4636308cb16fdecc21b4c5d03aa3e03 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 01:10:25 -0500 Subject: [PATCH 069/395] Extend a generic factory's local class returned through a constructor type A generic factory declared to return a constructor type (`Struct = (abstract new () => S) & { decode }`) whose every return gives back one of its own local classes (`return Base as Struct`) is now that local class's factory: each instance has its own class, the result maps to the class value while the instance lowers and at its calls, and `class X extends struct(...)` extends it like a factory returning the class directly. Before, the result type mapped to nothing (SC2008) and the extends clause refused as a computed expression. Static methods of a function-local class in a TypeScript file now lower as the class value's runtime statics, as they already do in JavaScript files: they read the enclosing function's bindings, call each other through `this`, and inherit into a subclass. Before, they refused with SC1090. Static fields and static blocks of such classes keep the fence. Fixes #1372 Co-authored-by: blurname <50226652+blurname@users.noreply.github.com> --- .../local-class-expressions.mdx | 3 +- .../frontend/lowering/generic-functions.ts | 123 ++++++++++++++++-- .../src/frontend/lowering/lower-classes.ts | 10 +- .../compiler/src/frontend/lowering/lowerer.ts | 10 ++ packages/compiler/src/frontend/type-mapper.ts | 11 ++ .../6912-extend-factory-local-class.ts.json | 6 + .../corpus/6912-extend-factory-local-class.ts | 67 ++++++++++ .../6912-extend-factory-local-class.ts.json | 1 + 8 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6912-extend-factory-local-class.ts.json create mode 100644 tests/corpus/6912-extend-factory-local-class.ts create mode 100644 tests/harness/corpus-costs/6912-extend-factory-local-class.ts.json 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 8b653b25eb..173bdc0c66 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,4 +1,5 @@ -- **Local class expressions:** Static members, 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, generic members, and references through the class's internal name are unsupported. + Static methods read the enclosing function's bindings and are inherited by subclasses. 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/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index abcd5a584a..0f549d889c 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -129,6 +129,10 @@ export interface GenericInstance { tsBindings?: Map; /** Rendered type arguments ("") for diagnostics. */ typeArgsText: string; + /** A factory returning one of its local classes through a declared + * constructor type: that type (by id) and the class value it maps to + * while the body lowers (Lowerer.instanceResultClass). */ + resultClass?: { declared: number; value: IrType }; /** Implicit instances only: param symbol → the call site's (widened) * checker type, consulted by the Lowerer's typeOf while this instance's * body lowers (the implicit twin of `bindings`). */ @@ -468,7 +472,8 @@ function localClassResultInstance( params: ParamShape[], retTs: ts.Type, ): GenericInstance | null { - if (!localClassOfResult(lowerer, info, retTs)) return null; + const resultClass = localClassOfResult(lowerer, info, retTs); + if (!resultClass) return null; const tsBindings = new Map(); const bindings = lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings); const bindingKey = info.typeParams @@ -483,12 +488,29 @@ function localClassResultInstance( tsBindings, }); if (inst.returnType !== pending) return inst; - const returnType = withInstanceScope(lowerer, info, inst, () => lowerer.mapTypeOf(retTs)); + // A result typed by a declared constructor type rather than the class + // (`Struct`) is the class value of the local class the body returns. + const declaredResult = info.decl.type + ? lowerer.checker.getTypeFromTypeNode(info.decl.type) + : undefined; + const viaConstructorType = retTs.getSymbol()?.valueDeclaration !== resultClass; + const returnType = withInstanceScope(lowerer, info, inst, () => { + if (!viaConstructorType) return lowerer.mapTypeOf(retTs); + const classSymbol = resultClass.name + ? lowerer.checker.getSymbolAtLocation(resultClass.name) + : undefined; + return classSymbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(classSymbol)) : null; + }); if (!returnType) { fenceGenericSignatureResult(lowerer, expr, retTs); lowerer.badType(expr, retTs); } inst.returnType = returnType; + if (viaConstructorType && returnType.kind === "classval") { + if (declaredResult) inst.resultClass = { declared: declaredResult.id, value: returnType }; + if (lowerer.typeParamBindings === null) lowerer.localClassValueTypes.set(retTs.id, returnType); + return inst; + } // Values of the class outside the factory (`const b = new B()`, `b.read()`) // carry the instantiated checker type; record which class it names. A // call inside another generic instance can have a type that depends on @@ -563,12 +585,94 @@ function localClassOfResult( type: ts.Type, ): ts.ClassLikeDeclaration | null { const symbol = type.getSymbol(); - if (!symbol || !(symbol.flags & ts.SymbolFlags.Class)) return null; - const decl = symbol.valueDeclaration; - if (!decl || !(ts.isClassExpression(decl) || ts.isClassDeclaration(decl))) return null; - for (let parent = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) - if (parent === info.decl) return decl; - return null; + const decl = symbol && symbol.flags & ts.SymbolFlags.Class ? symbol.valueDeclaration : undefined; + if (decl && (ts.isClassExpression(decl) || ts.isClassDeclaration(decl))) + return declaredInside(decl, info.decl) ? decl : null; + // A result typed by a constructor type (`Struct`, `new () => S`) whose + // every return gives back the one local class, through parentheses and + // assertions (`return Base as Struct`). + // A mixin (a class extending one of the function's parameters) keeps the + // mixin instantiation, which keys on the argument class. + if (lowerer.checker.getConstructSignatures(type).length === 0) return null; + const returned = returnedLocalClass(lowerer, info); + return returned && !extendsParameter(lowerer, returned, info) ? returned : null; +} + +function extendsParameter( + lowerer: Lowerer, + decl: ts.ClassLikeDeclaration, + info: GenericFnInfo, +): boolean { + const params = new Set(); + const fn = info.decl as { parameters?: { nodes: readonly ts.ParameterDeclaration[] } }; + for (const param of fn.parameters?.nodes ?? []) { + const symbol = ts.isIdentifier(param.name) + ? lowerer.checker.getSymbolAtLocation(param.name) + : undefined; + if (symbol !== undefined) params.add(symbol); + } + for (const clause of decl.heritageClauses?.nodes ?? []) + for (const base of clause.types.nodes) { + const symbol = ts.isIdentifier(base.expression) + ? lowerer.checker.getSymbolAtLocation(base.expression) + : undefined; + if (symbol !== undefined && params.has(symbol)) return true; + } + return false; +} + +function declaredInside(node: ts.Node, owner: ts.Node): boolean { + for (let parent = node.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) + if (parent === owner) return true; + return false; +} + +/** The local class every return of `info`'s body gives back, or null when a + * return gives anything else (or nothing). Nested functions and classes keep + * their own returns. */ +function returnedLocalClass(lowerer: Lowerer, info: GenericFnInfo): ts.ClassLikeDeclaration | null { + const fn = info.decl as ts.Node & { body?: ts.Node }; + const body = fn.body; + if (!body) return null; + const classOf = (node: ts.Expression): ts.ClassLikeDeclaration | null => { + let expr = node; + while ( + ts.isParenthesizedExpression(expr) || + ts.isAsExpression(expr) || + ts.isSatisfiesExpression(expr) || + ts.isNonNullExpression(expr) + ) + expr = expr.expression; + if (ts.isClassExpression(expr)) return declaredInside(expr, info.decl) ? expr : null; + if (!ts.isIdentifier(expr)) return null; + const declaration = lowerer.checker.getSymbolAtLocation(expr)?.valueDeclaration; + return declaration && + ts.isClassDeclaration(declaration) && + declaredInside(declaration, info.decl) + ? declaration + : null; + }; + if (!ts.isBlock(body)) return classOf(body as ts.Expression); + let found: ts.ClassLikeDeclaration | null = null; + let valid = true; + const visit = (node: ts.Node): void => { + if ( + !valid || + ts.isFunctionLike(node) || + ts.isClassDeclaration(node) || + ts.isClassExpression(node) + ) + return; + if (ts.isReturnStatement(node)) { + const returned = node.expression ? classOf(node.expression) : null; + if (!returned || (found !== null && found !== returned)) valid = false; + else found = returned; + return; + } + ts.forEachChild(node, visit); + }; + ts.forEachChild(body, visit); + return valid ? found : null; } /** Runs `fn` under an instance's type bindings and local-class names, as @@ -583,14 +687,17 @@ function withInstanceScope( const prevTsBindings = lowerer.typeParamTsBindings; const previousPath = lowerer.genericInstantiationPath; const prevContext = lowerer.instantiationContext; + const prevResultClass = lowerer.instanceResultClass; lowerer.typeParamBindings = inst.bindings; lowerer.typeParamTsBindings = inst.tsBindings ?? null; lowerer.genericInstantiationPath = inst.path; lowerer.instantiationContext = `instantiating '${info.baseName}' with ${inst.typeArgsText}`; + lowerer.instanceResultClass = inst.resultClass ?? null; lowerer.localClassInstantiations.push({ owner: info.decl, name: inst.name }); try { return fn(); } finally { + lowerer.instanceResultClass = prevResultClass; lowerer.localClassInstantiations.pop(); lowerer.typeParamBindings = prevBindings; lowerer.typeParamTsBindings = prevTsBindings; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 790efe988d..505b4d7095 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -2404,7 +2404,8 @@ export function collectClassShapeInner( for (const member of decl.members.nodes) { if ( (functionLocalClass(decl) || checkedStatics) && - isJsSourceFile(ts.getSourceFileOfNode(decl)) && + (isJsSourceFile(ts.getSourceFileOfNode(decl)) || + (functionLocalClass(decl) && ts.isMethodDeclaration(member))) && (ts.isClassStaticBlockDeclaration(member) || ts .getModifiers(member) @@ -5872,9 +5873,10 @@ export function lowerClassExpressionInfo( decoratorNodesOf(member).length || (!isJsSourceFile(ts.getSourceFileOfNode(expr)) && (ts.isClassStaticBlockDeclaration(member) || - ts - .getModifiers(member) - ?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword))) || + (!ts.isMethodDeclaration(member) && + ts + .getModifiers(member) + ?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)))) || (ts.isMethodDeclaration(member) && member.typeParameters?.nodes.length), )) ) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ee8ba7ecba..c1074f0a56 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2171,6 +2171,12 @@ export class Lowerer { /** Instance types (by checker type id) of the local classes generic * factory calls return, and the per-instance class each names. */ readonly localClassInstanceTypes = new Map(); + /** A generic factory's instantiated result type → the class value of the + * local class it returns (TypeMapperCtx.localClassValue). */ + readonly localClassValueTypes = new Map(); + /** While a generic factory instance lowers: its declared result type → the + * class value of the local class it returns. */ + instanceResultClass: { declared: number; value: IrType } | null = null; /** The ts-level twin of typeParamBindings, non-null while a CALL-keyed * instance body lowers: type-parameter symbol → the bound CHECKER type, * consulted where the mapped IrType has already widened away information @@ -3529,6 +3535,10 @@ export class Lowerer { genericClassInstance: (decl, ref, mapArgument) => this.genericClassInstanceType(decl, ref, mapArgument), runtimeClassValue: (type) => runtimeClassValueOfType(this, type), + localClassValue: (type) => + this.instanceResultClass?.declared === type.id + ? this.instanceResultClass.value + : (this.localClassValueTypes.get(type.id) ?? null), mixinClassInstance: (decl) => this.mixinTypeContext && this.mixinTypeContext.classNode === decl ? { kind: "object", className: this.mixinTypeContext.className } diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index a233f0186d..25b8c45a9e 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1114,6 +1114,12 @@ export interface TypeMapperCtx { * only the literal's objects, so it maps to the empty record (see the * empty object type rule in the object mapping). */ emptyLiteralArm?: boolean; + /** The class value a generic factory's result type names when the factory + * returns one of its own local classes through a constructor type + * (`function struct(v: S): Struct { class Base {}; return Base as + * Struct; }`), by the checker type: the declared result inside the + * instance being lowered, the instantiated result at its calls. */ + localClassValue?: (type: ts.Type) => IrType | null; checker: ts.TypeChecker; /** JavaScript declarations whose instance or object-literal types only * the checked-dynamic representation holds (js-dynamic-objects.ts). @@ -1653,6 +1659,11 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return STRING; if (flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) return BOOL; } + const localValue = ctx.localClassValue?.(type); + if (localValue) { + contextResolutions++; + return localValue; + } const topLevel = mapTypeDepth === 0; const typeId = (type as { id?: number }).id; const memoizable = typeId !== undefined && (ctx.canMemoizeType?.() ?? true); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6912-extend-factory-local-class.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6912-extend-factory-local-class.ts.json new file mode 100644 index 0000000000..5fc7da1eb6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6912-extend-factory-local-class.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6912-extend-factory-local-class.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6912-extend-factory-local-class.ts b/tests/corpus/6912-extend-factory-local-class.ts new file mode 100644 index 0000000000..8848280e27 --- /dev/null +++ b/tests/corpus/6912-extend-factory-local-class.ts @@ -0,0 +1,67 @@ +// A generic factory returns its own local class through a declared +// constructor type, and a class extends the factory's result. The local +// class's static methods read the factory's parameter, inherit into the +// subclass, and call each other through `this`. +type Struct = (abstract new () => S) & { readonly decode: (value: unknown) => S }; +function struct(value: S): Struct { + abstract class Base { + static decode(_input: unknown): S { + return value; + } + } + return Base as Struct; +} +class Counter extends struct({ value: 42 }) {} +console.log(Counter.decode(null).value); + +type Named = (new (label: string) => { label: string; payload: T }) & { + readonly describe: () => string; + readonly make: (label: string) => { label: string; payload: T }; +}; +function named(kind: string, payload: T): Named { + class Entry { + label: string; + payload: T; + constructor(label: string) { + this.label = label; + this.payload = payload; + } + static describe(): string { + return `entry of ${kind}`; + } + static make(label: string): Entry { + return new this(label); + } + } + return Entry; +} +class Tag extends named("tag", [1, 2, 3]) { + shout(): string { + return this.label.toUpperCase(); + } +} +const tag = new Tag("hello"); +console.log(tag.shout(), tag.payload.join("+"), Tag.describe()); +const made = Tag.make("made"); +console.log(made.label, made.payload.length); + +const Flags = named("flag", { on: true }); +const flag = new Flags("first"); +console.log(flag.label, flag.payload.on, Flags.describe()); +const Other = named("other", 5); +console.log(Other.describe(), new Other("x").payload, Flags.describe()); + +// A function-local class's static methods read the function's parameter and +// call each other through `this`. +function twice(n: number): number { + class K { + static double(): number { + return n * 2; + } + static quad(): number { + return this.double() * 2; + } + } + return K.double() + K.quad(); +} +console.log(twice(3), twice(5)); diff --git a/tests/harness/corpus-costs/6912-extend-factory-local-class.ts.json b/tests/harness/corpus-costs/6912-extend-factory-local-class.ts.json new file mode 100644 index 0000000000..a44d195f2f --- /dev/null +++ b/tests/harness/corpus-costs/6912-extend-factory-local-class.ts.json @@ -0,0 +1 @@ +{ "plain": 3633, "san": 5578 } From 0763610e93a464943833a0ccb498bf193d2f6eb0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 01:11:37 -0500 Subject: [PATCH 070/395] Version loops to a typed array's length on every typed array they index A loop such as `for (let i = 0; i < out.length; i++) out[i] = x[i] * y[i]` keeps its integer induction, and the version guard now also receives the loop's counted form, so it covers the accesses of `x` and `y` at the counter. Before, only counted loops (`i < n`) passed a counter to the guard, and loops to a length kept one bounds check per access of every other typed array, which kept LLVM from vectorizing them. Adds the typed-array-kernels runtime workload: element-wise loops over Float64Array, Float32Array, Uint8Array and Int32Array, and RGBA to gray. --- benchmarks/runtime/README.md | 1 + .../workloads/typed-array-kernels.json | 6 + .../runtime/workloads/typed-array-kernels.ts | 92 +++++++++++ .../backend/llvm/bytes-loop-versions.test.ts | 41 +++++ .../src/backend/llvm/emit-statements.ts | 21 ++- ...2-typed-array-length-loop-versions.ts.json | 6 + ...3-typed-array-length-loop-bindings.ts.json | 6 + .../6992-typed-array-length-loop-versions.ts | 141 +++++++++++++++++ .../6993-typed-array-length-loop-bindings.ts | 146 ++++++++++++++++++ ...2-typed-array-length-loop-versions.ts.json | 1 + ...3-typed-array-length-loop-bindings.ts.json | 1 + 11 files changed, 458 insertions(+), 4 deletions(-) create mode 100644 benchmarks/runtime/workloads/typed-array-kernels.json create mode 100644 benchmarks/runtime/workloads/typed-array-kernels.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6992-typed-array-length-loop-versions.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6993-typed-array-length-loop-bindings.ts.json create mode 100644 tests/corpus/6992-typed-array-length-loop-versions.ts create mode 100644 tests/corpus/6993-typed-array-length-loop-bindings.ts create mode 100644 tests/harness/corpus-costs/6992-typed-array-length-loop-versions.ts.json create mode 100644 tests/harness/corpus-costs/6993-typed-array-length-loop-bindings.ts.json diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index d736a4ed4b..1acdc9ce7c 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -26,6 +26,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds | | `template-render` | cpu | template literals, escaping, `map`/`join` | | `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread | +| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, RGBA to gray | | `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage | | `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts | | `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains | diff --git a/benchmarks/runtime/workloads/typed-array-kernels.json b/benchmarks/runtime/workloads/typed-array-kernels.json new file mode 100644 index 0000000000..fff290679c --- /dev/null +++ b/benchmarks/runtime/workloads/typed-array-kernels.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/typed-array-kernels.ts", + "args": [ + "1" + ] +} diff --git a/benchmarks/runtime/workloads/typed-array-kernels.ts b/benchmarks/runtime/workloads/typed-array-kernels.ts new file mode 100644 index 0000000000..87c227bbc3 --- /dev/null +++ b/benchmarks/runtime/workloads/typed-array-kernels.ts @@ -0,0 +1,92 @@ +// Element-wise loops over several typed arrays: each loop runs to one +// array's length and reads or writes the others at the same index. + +function axpy(a: number, x: Float64Array, y: Float64Array, out: Float64Array): void { + for (let i = 0; i < out.length; i++) out[i] = a * x[i]! + y[i]!; +} + +function blend(t: number, a: Float32Array, b: Float32Array, out: Float32Array): void { + for (let i = 0; i < out.length; i++) out[i] = a[i]! * (1 - t) + b[i]! * t; +} + +function mask(data: Uint8Array, key: Uint8Array, out: Uint8Array): void { + for (let i = 0; i < out.length; i++) out[i] = data[i]! ^ key[i]!; +} + +function addWrap(a: Int32Array, b: Int32Array, out: Int32Array): void { + for (let i = 0; i < out.length; i++) out[i] = (a[i]! + b[i]!) | 0; +} + +function gray(rgba: Uint8Array, out: Uint8Array): void { + for (let i = 0; i < out.length; i++) { + const j = i * 4; + out[i] = (rgba[j]! * 77 + rgba[j + 1]! * 150 + rgba[j + 2]! * 29) >> 8; + } +} + +const scale = Number(process.argv[2] ?? "1"); +const n = 1 << 16; +const x = new Float64Array(n); +const y = new Float64Array(n); +const d = new Float64Array(n); +const fa = new Float32Array(n); +const fb = new Float32Array(n); +const fo = new Float32Array(n); +const data = new Uint8Array(n); +const key = new Uint8Array(n); +const masked = new Uint8Array(n); +const ia = new Int32Array(n); +const ib = new Int32Array(n); +const io = new Int32Array(n); +const rgba = new Uint8Array(n * 4); +const luma = new Uint8Array(n); +let state = 2463534242; +const next = (): number => { + state ^= state << 13; + state ^= state >>> 17; + state ^= state << 5; + return state >>> 0; +}; +for (let i = 0; i < n; i++) { + x[i] = (next() % 20000) / 100 - 100; + y[i] = (next() % 20000) / 100; + fa[i] = (next() % 1000) / 7; + fb[i] = (next() % 1000) / 9; + data[i] = next() & 255; + key[i] = next() & 255; + ia[i] = next() | 0; + ib[i] = next() | 0; +} +for (let i = 0; i < rgba.length; i++) rgba[i] = next() & 255; + +const rounds = Math.round(1200 * scale); +let f64Sum = 0; +let f32Sum = 0; +let byteSum = 0; +let intSum = 0; +let lumaSum = 0; +for (let r = 0; r < rounds; r++) { + // Change one input element per round so no round repeats the last. + const at = (r * 7919) & (n - 1); + x[at] = r; + fa[at] = r; + data[at] = r & 255; + ia[at] = r * 65537; + rgba[at * 4] = r & 255; + axpy(1.5, x, y, d); + blend((r & 15) / 16, fa, fb, fo); + mask(data, key, masked); + addWrap(ia, ib, io); + gray(rgba, luma); + const probe = (r * 104729) & (n - 1); + f64Sum += d[probe]!; + f32Sum += fo[probe]!; + byteSum += masked[probe]!; + intSum = (intSum + io[probe]!) | 0; + lumaSum += luma[probe]!; +} +console.log(`float64 ${f64Sum.toFixed(3)}`); +console.log(`float32 ${f32Sum.toFixed(3)}`); +console.log(`uint8 ${byteSum}`); +console.log(`int32 ${intSum}`); +console.log(`gray ${lumaSum}`); 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 ddcfd667d1..ea568bc3f8 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -209,6 +209,47 @@ test("emitted loops version the body and tag typed-array memory", () => { expect(elements).not.toBe(header); }); +test("loops to a typed array's length version the other typed arrays they index", () => { + const length = (receiver: IrExpr): IrExpr => ({ + kind: "bytesIntrinsic", + method: "length", + receiver, + args: [], + type: F64, + loc, + }); + const emit = (body: IrStmt[]): string => { + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + fn(body), + ], + }; + return /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + }; + // for (let i = 0; i < out.length; i++) out[i] = src[i] * 2 + const scaled = set(ref("out", f64s), ref("i"), bin("*", get(ref("src", f64s), ref("i")), num(2))); + const body = emit([counted([scaled], length(ref("out", f64s)))]); + const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0]; + // The loop keeps its integer induction, and the fast copy reads `src` + // unchecked. + expect(body).toContain("integer induction i"); + expect(fast).not.toContain("bytes.index"); + expect(body.slice(0, body.indexOf(fast))).toContain("bytes.index.invalid"); + // A body that rebinds `src` keeps one checked loop. + const rebound = emit([ + counted( + [scaled, { kind: "assign", localId: "src", value: ref("out", f64s), loc }], + length(ref("out", f64s)), + ), + ]); + expect(rebound).not.toContain("bytes.version.fast"); + expect(rebound).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/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index d1dc6369b8..4330296519 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -945,12 +945,25 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.startBlock(done); break; } - const version = + const versionable = emitter.bytesLoopVersionsEnabled && emitter.bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH && - !emitter.bytesLoopVersioning.has(s) - ? emitter.matchBytesLoopVersion(s, countedLoop) - : null; + !emitter.bytesLoopVersioning.has(s); + // A loop to a typed array's or an array's length keeps its integer + // induction. Its counted form only gives the version guard the + // counter's iteration space, so the guard also covers accesses of + // other typed arrays at the counter, such as `x[i]` and `y[i]` in + // `for (let i = 0; i < out.length; i++) out[i] = x[i] * y[i]`. + const versionCounted = + versionable && !countedLoop && integerLoop && emitter.countedLoopsEnabled + ? matchIntegerCountedForLoop( + s, + emitter.numericLocals, + emitter.integerRanges, + emitter.unaliasedArrays, + ) + : countedLoop; + const version = versionable ? emitter.matchBytesLoopVersion(s, versionCounted) : null; if (version) { const fast = B.newLabel("bytes.version.fast"), slow = B.newLabel("bytes.version.slow"), diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6992-typed-array-length-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6992-typed-array-length-loop-versions.ts.json new file mode 100644 index 0000000000..2d7025954d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6992-typed-array-length-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6992-typed-array-length-loop-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6993-typed-array-length-loop-bindings.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6993-typed-array-length-loop-bindings.ts.json new file mode 100644 index 0000000000..43b9c5f094 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6993-typed-array-length-loop-bindings.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6993-typed-array-length-loop-bindings.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6992-typed-array-length-loop-versions.ts b/tests/corpus/6992-typed-array-length-loop-versions.ts new file mode 100644 index 0000000000..eb08f7145f --- /dev/null +++ b/tests/corpus/6992-typed-array-length-loop-versions.ts @@ -0,0 +1,141 @@ +// Loops bounded by one typed array's length that index other typed arrays: +// equal and different lengths, offsets, mixed element types and views that +// share a buffer. Out-of-bounds reads give undefined (NaN in arithmetic) and +// out-of-bounds writes are ignored, whichever array is shorter. + +function mulInto(x: Float32Array, y: Float32Array, out: Float32Array): void { + for (let i = 0; i < out.length; i++) out[i] = x[i]! * y[i]!; +} + +function axpy(a: number, x: Float64Array, y: Float64Array, out: Float64Array): void { + for (let i = 0; i < out.length; i++) out[i] = a * x[i]! + y[i]!; +} + +function xorInto(a: Uint8Array, b: Uint8Array, out: Uint8Array): void { + for (let i = 0; i < out.length; i++) out[i] = a[i]! ^ b[i]!; +} + +function addWrap(a: Int32Array, b: Int32Array, out: Int32Array): void { + for (let i = 0; i < out.length; i++) out[i] = (a[i]! + b[i]!) | 0; +} + +function narrow(src: Float64Array, i8: Int8Array, u16: Uint16Array, u32: Uint32Array): void { + for (let i = 0; i < src.length; i++) { + i8[i] = src[i]!; + u16[i] = src[i]!; + u32[i] = src[i]!; + } +} + +function neighbors(a: Int32Array, out: Int32Array): void { + for (let i = 0; i < a.length; i++) out[i] = a[i + 1]! - a[i]!; +} + +function previous(a: Float64Array, out: Float64Array): void { + for (let i = 0; i < out.length; i++) out[i] = a[i - 1]!; +} + +function gray(px: Uint8Array, out: Uint8Array): void { + for (let i = 0; i < out.length; i++) { + const j = i * 4; + out[i] = (px[j]! * 77 + px[j + 1]! * 150 + px[j + 2]! * 29) >> 8; + } +} + +function sumPairs(a: Float64Array, b: Float64Array): number { + let s = 0; + for (let i = 0; i < a.length; i++) s += a[i]! * b[i]!; + return s; +} + +function shiftUp(view: Uint8Array, from: Uint8Array): void { + for (let i = 0; i < view.length; i++) view[i] = from[i]! + 1; +} + +const n = 37; +const fx = new Float32Array(n); +const fy = new Float32Array(n); +const dx = new Float64Array(n); +const dy = new Float64Array(n); +const ba = new Uint8Array(n); +const bb = new Uint8Array(n); +const ia = new Int32Array(n); +const ib = new Int32Array(n); +for (let i = 0; i < n; i++) { + fx[i] = i / 3; + fy[i] = 0.1 * i - 1.7; + dx[i] = i * 1.25 - 9; + dy[i] = (i * 7) % 11; + ba[i] = (i * 29) & 255; + bb[i] = (i * 113 + 7) & 255; + ia[i] = i * 0x3fffffff; + ib[i] = i * -0x2ffffff1; +} + +const fo = new Float32Array(n); +mulInto(fx, fy, fo); +console.log("f32", fo.join(",")); +const shortF = new Float32Array(5); +mulInto(fx, fy.subarray(0, 3), shortF); +console.log("f32 short input", shortF.join(",")); +const longF = new Float32Array(n + 4); +mulInto(fx, fy, longF); +console.log("f32 long output", longF.subarray(n - 2).join(",")); + +const dout = new Float64Array(n); +axpy(-2.5, dx, dy, dout); +console.log("axpy", dout.join(",")); +axpy(0.5, dx.subarray(10), dy, dout); +console.log("axpy short x", dout.subarray(20).join(",")); + +const bo = new Uint8Array(n); +xorInto(ba, bb, bo); +console.log("xor", bo.join(",")); +const io = new Int32Array(n); +addWrap(ia, ib, io); +console.log("i32", io.join(",")); + +const src = new Float64Array([0, -1, 1.5, -128.5, 255.7, 256, 65535.9, -65537, 4294967296.5, NaN, Infinity, -0]); +const i8 = new Int8Array(src.length); +const u16 = new Uint16Array(src.length - 2); +const u32 = new Uint32Array(src.length + 1); +narrow(src, i8, u16, u32); +console.log("narrow", i8.join(","), "|", u16.join(","), "|", u32.join(",")); + +const diff = new Int32Array(n); +neighbors(ia, diff); +console.log("neighbors", diff.subarray(n - 3).join(",")); +const before = new Float64Array(4); +previous(dx, before); +console.log("previous", before.join(",")); + +const px = new Uint8Array(n * 4); +for (let i = 0; i < px.length; i++) px[i] = (i * 53) & 255; +const g = new Uint8Array(n); +gray(px, g); +console.log("gray", g.join(",")); +const wide = new Uint8Array(n + 3); +gray(px, wide); +console.log("gray past input", wide.subarray(n - 1).join(",")); + +console.log("dot", sumPairs(dx, dy), sumPairs(dx, dy.subarray(0, 30))); + +// Views of one buffer: writes through one are visible to reads through the +// other on later iterations. +const buffer = new Uint8Array(24); +for (let i = 0; i < buffer.length; i++) buffer[i] = (i * 37) & 255; +shiftUp(buffer.subarray(1), buffer); +console.log("overlap forward", buffer.join(",")); +for (let i = 0; i < buffer.length; i++) buffer[i] = (i * 37) & 255; +shiftUp(buffer, buffer.subarray(1)); +console.log("overlap backward", buffer.join(",")); +const shared = new ArrayBuffer(64); +const asFloat = new Float32Array(shared); +const asBytes = new Uint8Array(shared, 4, 16); +for (let i = 0; i < asFloat.length; i++) asFloat[i] = i + 0.5; +const fromBytes = new Float32Array(16); +for (let i = 0; i < fromBytes.length; i++) { + asBytes[i] = i * 3; + fromBytes[i] = asFloat[i]!; +} +console.log("mixed views", fromBytes.join(","), asBytes.join(",")); diff --git a/tests/corpus/6993-typed-array-length-loop-bindings.ts b/tests/corpus/6993-typed-array-length-loop-bindings.ts new file mode 100644 index 0000000000..e4b242b28b --- /dev/null +++ b/tests/corpus/6993-typed-array-length-loop-bindings.ts @@ -0,0 +1,146 @@ +// Loops bounded by a typed array's length whose bindings change while they +// run, that leave early, or that run at module level, in generators and in +// async functions. Each keeps the reads, writes and bounds the source names +// on every iteration. + +function swapBound(a: Float64Array, b: Float64Array, out: Float64Array): number { + let n = 0; + for (let i = 0; i < a.length; i++) { + out[i] = a[i]! + b[i]!; + n++; + if (i === 2) a = b; + } + return n; +} + +function swapOperand(a: Int16Array, b: Int16Array, out: Int16Array): void { + for (let i = 0; i < out.length; i++) { + out[i] = a[i]! * 3; + if (i === 3) a = b; + } +} + +function closureSwap(a: Uint16Array, b: Uint16Array, out: Uint16Array): void { + const swap = (): void => { + out = b; + }; + for (let i = 0; i < out.length; i++) { + out[i] = a[i]! + 1000; + if (i === 1) swap(); + } +} + +function firstAbove(values: Float32Array, limits: Float32Array): number { + for (let i = 0; i < values.length; i++) { + if (values[i]! > limits[i]!) return i; + } + return -1; +} + +function copyUntilZero(src: Uint8Array, out: Uint8Array): number { + let copied = 0; + for (let i = 0; i < src.length; i++) { + if (src[i] === 0) break; + out[i] = src[i]!; + copied++; + } + return copied; +} + +function checkedScale(src: Int32Array, out: Int32Array): void { + for (let i = 0; i < src.length; i++) { + if (src[i]! < 0) throw new RangeError(`negative at ${i}: ${src[i]}`); + out[i] = src[i]! * 2; + } +} + +function fromList(values: number[], out: Float64Array): void { + for (let i = 0; i < values.length; i++) { + out[i] = values[i]! / 2; + if (values.length < 6) values.push(i * 10); + } +} + +function rows(table: Uint32Array[], totals: Uint32Array): void { + for (let r = 0; r < table.length; r++) { + const row = table[r]!; + let sum = 0; + for (let c = 0; c < row.length; c++) sum += row[c]! * (c + 1); + totals[r] = sum; + } +} + +function* pairs(a: Int8Array, b: Int8Array): Generator { + for (let i = 0; i < a.length; i++) yield a[i]! * b[i]!; +} + +async function addLater(a: Float64Array, b: Float64Array, out: Float64Array): Promise { + for (let i = 0; i < out.length; i++) { + out[i] = a[i]! + b[i]!; + if (i % 2 === 0) await null; + } +} + +const a = new Float64Array([1, 2, 3, 4, 5, 6]); +const b = new Float64Array([10, 20, 30]); +const sum = new Float64Array(8); +console.log("swap bound", swapBound(a, b, sum), sum.join(",")); + +const s16 = new Int16Array([1, -2, 3, -4, 5, -6, 7]); +const t16 = new Int16Array([100, 200, 300, 400, 500, 600]); +const o16 = new Int16Array(7); +swapOperand(s16, t16, o16); +console.log("swap operand", o16.join(",")); + +const u = new Uint16Array([5, 6, 7, 8]); +const other = new Uint16Array(2); +const first = new Uint16Array(4); +closureSwap(u, other, first); +console.log("closure swap", first.join(","), other.join(",")); + +const values = new Float32Array([0.5, 1.5, 2.5, 3.5]); +console.log("first above", firstAbove(values, new Float32Array([1, 1, 1, 1]))); +console.log("first above short", firstAbove(values, new Float32Array([9]))); +console.log("first above none", firstAbove(values, new Float32Array([9, 9, 9, 9]))); + +const text = new Uint8Array([72, 105, 33, 0, 88]); +const copy = new Uint8Array(2); +console.log("copy", copyUntilZero(text, copy), copy.join(",")); + +const ints = new Int32Array([4, 8, -15, 16]); +const doubled = new Int32Array(4); +try { + checkedScale(ints, doubled); +} catch (error) { + console.log((error as Error).name, (error as Error).message, doubled.join(",")); +} + +const list = [3, 5]; +const halves = new Float64Array(5); +fromList(list, halves); +console.log("from list", list.join(","), halves.join(",")); + +const table = [new Uint32Array([1, 2, 3]), new Uint32Array([]), new Uint32Array([4294967295, 1])]; +const totals = new Uint32Array(4); +rows(table, totals); +console.log("rows", totals.join(",")); + +console.log("pairs", [...pairs(new Int8Array([1, -2, 3, 127]), new Int8Array([4, 5, -6]))].join(",")); + +const left = new Float64Array([0.25, 0.5, 0.75, 1]); +const right = new Float64Array([1, 2, 3, 4, 5]); +const later = new Float64Array(5); +const pending = addLater(left, right, later); +console.log("before await", later.join(",")); + +// Module-level bindings, including one a loop body reassigns. +let source = new Uint8Array([9, 8, 7, 6, 5]); +const replacement = new Uint8Array([1, 2]); +const target = new Uint8Array(5); +for (let i = 0; i < source.length; i++) { + target[i] = source[i]! + 100; + if (i === 1) source = replacement; +} +console.log("module", target.join(","), source.join(",")); + +pending.then(() => console.log("after await", later.join(","))); diff --git a/tests/harness/corpus-costs/6992-typed-array-length-loop-versions.ts.json b/tests/harness/corpus-costs/6992-typed-array-length-loop-versions.ts.json new file mode 100644 index 0000000000..26208456e3 --- /dev/null +++ b/tests/harness/corpus-costs/6992-typed-array-length-loop-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 3800, "san": 5600 } diff --git a/tests/harness/corpus-costs/6993-typed-array-length-loop-bindings.ts.json b/tests/harness/corpus-costs/6993-typed-array-length-loop-bindings.ts.json new file mode 100644 index 0000000000..a6e281d1ea --- /dev/null +++ b/tests/harness/corpus-costs/6993-typed-array-length-loop-bindings.ts.json @@ -0,0 +1 @@ +{ "plain": 2700, "san": 4000 } From c081d5a3a771da3b09b3c6d6ec4d90f3e311bb3e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 01:16:55 -0500 Subject: [PATCH 071/395] Apply the byte offset in Buffer.from over untyped ArrayBuffers Buffer.from(arrayBuffer, byteOffset) with an untyped ArrayBuffer or SharedArrayBuffer now returns a Buffer from that offset to the end, as Node's fromArrayBuffer does, where the offset was dropped and the Buffer covered the whole buffer. --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/containers/bytes.ts | 11 +++-- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 1 + packages/runtime/src/scr_bytes.h | 1 + packages/runtime/src/scr_bytes_io.c | 43 +++++++++++++++++-- .../6868-buffer-from-untyped-array-buffer.cjs | 22 +++++++++- 7 files changed, 71 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 6832117626..cf2255f89c 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -448,6 +448,7 @@ export const LIB_FN_SYMS: Record = { "buffer.poolFromArray": "scr_buffer_from_arr", "buffer.poolFromDyn": "scr_buffer_from_dyn_pooled", "buffer.poolFromDynRef": "scr_buffer_from_dyn_ref_pooled", + "buffer.poolFromDynOffset": "scr_buffer_from_dyn_offset_pooled", "buffer.poolSize": "scr_buffer_pool_size", "buffer.setPoolSize": "scr_buffer_set_pool_size", "buffer.byteLenDyn": "scr_buffer_byte_length_dyn", diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index bf9821d317..afd108eb4d 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1743,8 +1743,9 @@ function knownBufferProducer( /** Buffer.from over a checked-dynamic source and encoding, both already in * repeatable locals: Node's fromString takes utf8 for an absent, non-string - * or empty encoding and throws ERR_UNKNOWN_ENCODING for an unknown name; - * other sources ignore the encoding. */ + * or empty encoding and throws ERR_UNKNOWN_ENCODING for an unknown name; an + * ArrayBuffer takes the second argument as its byte offset; other sources + * ignore it. */ function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrExpr { const utf8: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; const isString = (x: IrExpr): IrExpr => ({ @@ -1786,10 +1787,12 @@ function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrEx type: BYTES_U8, loc, }, + // An ArrayBuffer takes the second argument as its byte offset + // (scr_buffer_from_dyn_offset_pooled); other values convert with utf8. else_: { kind: "libCall", - fn: "buffer.poolFromDynRef", - args: [source, utf8], + fn: "buffer.poolFromDynOffset", + args: [source, encoding], type: BYTES_U8, loc, }, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 482739db5a..ae6b663ca6 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -789,6 +789,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "buffer.poolFromArray", "buffer.poolFromDyn", "buffer.poolFromDynRef", + "buffer.poolFromDynOffset", // The checked-dynamic compare/equals validators: Node's argument // ladders throw ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE catchably. "buffer.compareChk", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 49683550e0..c92cd40d41 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2921,6 +2921,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "buffer.poolFromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, /** poolFromDyn that also copies an array crossing by reference. */ "buffer.poolFromDynRef": { argTypes: [DYN, STRING], result: BYTES_U8 }, + "buffer.poolFromDynOffset": { argTypes: [DYN, DYN], result: BYTES_U8 }, "buffer.poolSize": { argTypes: [], result: F64 }, "buffer.setPoolSize": { argTypes: [F64], result: VOID }, /** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 9c81caea5f..9b382dda5f 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -397,6 +397,7 @@ ScrBytes *scr_buffer_from_bytes(const ScrBytes *src); /* a copy of any typed arr ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding); ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *encoding); +ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *second); ScrBytes *scr_buffer_concat(const ScrArr *list); ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total); /* The untyped-list forms: Node's ERR_INVALID_ARG_TYPE checks on the list diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index ac8fad02e8..bd79d9089a 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1300,9 +1300,10 @@ ScrBytes *scr_buffer_concat_len_dyn(const ScrDyn *list, double total) { } /* Buffer.from(arrayBuffer) over a checked value: a Buffer over all of the - * ArrayBuffer or SharedArrayBuffer that shares its memory, as in Node. A - * second argument would be the byte offset, and an encoding name converts - * to NaN, which is offset 0. */ + * ArrayBuffer or SharedArrayBuffer that shares its memory, as in Node. The + * calls that reach it have no second argument or a literal encoding name, + * which as a byte offset converts to NaN, offset 0; other second arguments + * take scr_buffer_from_dyn_offset_pooled, which applies the offset. */ static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage) { ScrBytes *out = scr_bytes_buffer_view(storage->v.handle.ptr, SCR_BYTES_U8, 0, false, 0); if (out) out->is_buffer = true; @@ -1381,6 +1382,42 @@ ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *enco return scr_buffer_from_dyn_pooled(value, encoding); } +/* Buffer.from(value, second) over checked values whose second argument is + * not a literal encoding name: an ArrayBuffer or SharedArrayBuffer takes it + * as the byte offset, as Node's fromArrayBuffer does (undefined and NaN are + * 0, the rest of the buffer is the length, ERR_BUFFER_OUT_OF_BOUNDS past the + * end); other values convert as scr_buffer_from_dyn_ref_pooled with utf8. */ +ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *second) { + if (!scr_buffer_storage_is(value)) { + ScrStr *utf8 = scr_str_new("utf8", 4); + ScrBytes *out = scr_buffer_from_dyn_ref_pooled(value, utf8); + scr_str_release(utf8); + return out; + } + double offset = 0; + if (second->kind != SCR_DYN_UNDEF) { + if (!scr_dyn_number_coerce_js(second, &offset)) return NULL; + if (isnan(offset)) offset = 0; + } + ScrBytes *storage = value->v.handle.ptr; + ScrBytes *owner = storage->backing ? storage->backing : storage; + if ((double)scr_bytes_byte_len(owner) - offset < 0) { + static const char msg[] = "\"offset\" is outside of buffer bounds"; + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS"); + return NULL; + } + double start = trunc(offset); + if (start < 0) { + char msg[96]; + int n = snprintf(msg, sizeof msg, "Start offset %.0f is outside the bounds of the buffer", start); + scr_throw_error_msg(SCR_ERR_RANGE, msg, (size_t)n); + return NULL; + } + ScrBytes *out = scr_bytes_buffer_view(storage, SCR_BYTES_U8, start, false, 0); + if (out) out->is_buffer = true; + return out; +} + /* new Buffer(number, encoding) — the deprecated ctor's string arm with a * non-string first argument: Node's exact ERR_INVALID_ARG_TYPE (the * throwing path never fires DEP0005, so compiled silence matches). */ diff --git a/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs b/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs index f525f24cf6..e30b5873e0 100644 --- a/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs +++ b/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs @@ -1,7 +1,8 @@ // Buffer.from over an untyped ArrayBuffer or SharedArrayBuffer (read from a // Map, an untyped object field or a worker message): the Buffer covers the -// whole buffer and shares its memory, in both directions, as in Node. An -// encoding name in the second argument converts to byte offset 0. +// buffer from its byte offset (the second argument) and shares its memory, in +// both directions, as in Node. An encoding name or NaN in the second argument +// is offset 0; an offset past the end throws ERR_BUFFER_OUT_OF_BOUNDS. 'use strict'; const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads'); @@ -19,6 +20,23 @@ if (isMainThread) { console.log(key, 'with encoding', encoded.length, encoded.toString('hex')); } + const offsets = new Map(); + offsets.set('storage', new ArrayBuffer(8)); + const storage = offsets.get('storage'); + for (const offset of [2, 1.5, '3', NaN, null, undefined, true, 8, 9, -1, -0.5]) { + try { + const view = Buffer.from(storage, offset); + view.fill(offset === 2 ? 0x11 : 0); + console.log('offset', String(offset), view.byteOffset, view.length, new Uint8Array(storage).join(',')); + } catch (error) { + console.log('offset', String(offset), error.name, error.code, error.message); + } + } + const sharedOffsets = new Map([['s', new SharedArrayBuffer(6)]]); + const tail = Buffer.from(sharedOffsets.get('s'), 4); + tail[0] = 0x7f; + console.log('shared offset', tail.length, new Uint8Array(sharedOffsets.get('s')).join(',')); + const holder = JSON.parse('{}'); holder.data = new ArrayBuffer(3); const view = Buffer.from(holder.data); From 777bfb43f2e126e5d5fa0f8046eca3944d87548d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 01:40:17 -0500 Subject: [PATCH 072/395] Revert "Name lowered helpers and closures independently of lowering order" This reverts #1750 (05e2f404d), which made main CI fail. --- package.json | 2 +- packages/compiler/src/contracts/check.ts | 3 +- .../lowering/array-indexed-mutation.ts | 3 +- .../src/frontend/lowering/array-values.ts | 5 +- .../frontend/lowering/builtins/arguments.ts | 3 +- .../lowering/builtins/child-process-async.ts | 3 +- .../lowering/builtins/child-process-sync.ts | 3 +- .../lowering/builtins/disposable-stack.ts | 3 +- .../frontend/lowering/builtins/filesystem.ts | 3 +- .../src/frontend/lowering/builtins/json.ts | 3 +- .../lowering/builtins/operating-system.ts | 7 +- .../frontend/lowering/builtins/performance.ts | 5 +- .../src/frontend/lowering/builtins/process.ts | 5 +- .../frontend/lowering/builtins/promises.ts | 5 +- .../lowering/builtins/promisify-callback.ts | 3 +- .../frontend/lowering/builtins/text-codecs.ts | 3 +- .../src/frontend/lowering/builtins/timers.ts | 2 +- .../src/frontend/lowering/builtins/url.ts | 7 +- .../src/frontend/lowering/builtins/v8.ts | 3 +- .../src/frontend/lowering/call-arguments.ts | 3 +- .../src/frontend/lowering/call-signatures.ts | 7 +- .../src/frontend/lowering/class-assertions.ts | 5 +- .../src/frontend/lowering/class-unions.ts | 3 +- .../lowering/coercions/function-adapters.ts | 11 +- .../frontend/lowering/coercions/structural.ts | 15 ++- .../src/frontend/lowering/coercions/unions.ts | 15 ++- .../frontend/lowering/collection-ingestion.ts | 3 +- .../lowering/containers/array-callbacks.ts | 25 ++-- .../lowering/containers/array-concat.ts | 3 +- .../lowering/containers/array-construction.ts | 7 +- .../lowering/containers/array-methods.ts | 11 +- .../lowering/containers/array-pipelines.ts | 3 +- .../src/frontend/lowering/containers/bytes.ts | 5 +- .../lowering/containers/callback-arguments.ts | 3 +- .../containers/collection-grouping.ts | 5 +- .../lowering/containers/collection-methods.ts | 15 ++- .../lowering/containers/indexed-objects.ts | 15 ++- .../lowering/containers/iterator-helpers.ts | 7 +- .../lowering/containers/string-and-regexp.ts | 11 +- .../lowering/containers/string-replacement.ts | 3 +- .../frontend/lowering/expressions/binary.ts | 3 +- .../lowering/expressions/object-literals.ts | 7 +- .../lowering/expressions/optional-chains.ts | 10 +- .../lowering/expressions/property-access.ts | 3 +- .../lowering/expressions/relational.ts | 5 +- .../frontend/lowering/generic-functions.ts | 17 +-- .../src/frontend/lowering/intern-tables.ts | 7 +- .../frontend/lowering/iterator-adapters.ts | 17 ++- .../frontend/lowering/js-dynamic-objects.ts | 5 +- .../src/frontend/lowering/json-require.ts | 3 +- .../src/frontend/lowering/lifetime-regions.ts | 3 +- .../src/frontend/lowering/lower-assert.ts | 11 +- .../src/frontend/lowering/lower-calls.ts | 49 ++++---- .../src/frontend/lowering/lower-classes.ts | 9 +- .../src/frontend/lowering/lower-dgram.ts | 5 +- .../frontend/lowering/lower-event-emitter.ts | 13 +- .../src/frontend/lowering/lower-exprs.ts | 9 +- .../src/frontend/lowering/lower-functions.ts | 2 +- .../src/frontend/lowering/lower-inspect.ts | 7 +- .../src/frontend/lowering/lower-mixins.ts | 3 +- .../src/frontend/lowering/lower-mock.ts | 3 +- .../frontend/lowering/lower-modules.test.ts | 4 +- .../src/frontend/lowering/lower-modules.ts | 15 +-- .../src/frontend/lowering/lower-namespaces.ts | 11 +- .../src/frontend/lowering/lower-server.ts | 13 +- .../src/frontend/lowering/lower-stdlib.ts | 2 +- .../src/frontend/lowering/lower-stream.ts | 15 ++- .../src/frontend/lowering/lower-timers.ts | 7 +- .../src/frontend/lowering/lowerer.test.ts | 30 ++--- .../compiler/src/frontend/lowering/lowerer.ts | 101 +++------------- .../src/frontend/lowering/native-ffi.ts | 2 +- .../lowering/object-enumeration-receiver.ts | 3 +- .../src/frontend/lowering/pairs-snapshot.ts | 3 +- .../lowering/promise-executor-adapter.ts | 3 +- .../lowering/require-initialization.ts | 3 +- .../src/frontend/lowering/stable-names.ts | 48 -------- .../src/frontend/lowering/strict-equality.ts | 3 +- .../frontend/lowering/structural-positions.ts | 107 ----------------- packages/compiler/src/frontend/ts7/ast.ts | 90 +------------- .../compiler/src/frontend/type-mapper.test.ts | 2 +- packages/compiler/src/frontend/type-mapper.ts | 92 ++------------ .../test/array-pipeline-fusion.test.ts | 2 +- .../test/format-float-arguments.test.ts | 2 +- .../test/in-runtime-key-emission.test.ts | 2 +- packages/compiler/test/stable-names.test.ts | 112 ------------------ .../compiler/test/type-mapper-unions.test.ts | 3 +- 86 files changed, 258 insertions(+), 854 deletions(-) delete mode 100644 packages/compiler/src/frontend/lowering/stable-names.ts delete mode 100644 packages/compiler/src/frontend/lowering/structural-positions.ts delete mode 100644 packages/compiler/test/stable-names.test.ts diff --git a/package.json b/package.json index d1a8e34ae0..5cc30d9f80 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test:test262": "tsx tests/test262/run.ts", "test:test262:fetch": "node tests/test262/fetch.mjs", "test:cache-identity": "node tests/harness/cache-identity.mjs", - "test:lowering-order": "node scripts/lowering-order.mjs --fail-on names", + "test:lowering-order": "node scripts/lowering-order.mjs --fail-on content", "test:sandbox": "node scripts/sandbox-test.mjs", "test:sandbox:image": "node scripts/sandbox-image.mjs", "test:watch": "vitest", diff --git a/packages/compiler/src/contracts/check.ts b/packages/compiler/src/contracts/check.ts index c804689615..5934624bf7 100644 --- a/packages/compiler/src/contracts/check.ts +++ b/packages/compiler/src/contracts/check.ts @@ -174,8 +174,7 @@ class ContractChecker { return `${owner}.${method[2]}`; } if (fn?.sourceName !== undefined && fn.sourceName !== "") return fn.sourceName; - if (fn !== undefined && /^%fn\.[0-9a-f]{12}$/.test(fnName)) - return `the function at ${this.where(fn.loc)}`; + if (fn !== undefined && /^%fn\d+$/.test(fnName)) return `the function at ${this.where(fn.loc)}`; return fnName.replace(/^%/, ""); } diff --git a/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts b/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts index 0ded460da3..6d539212e2 100644 --- a/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts +++ b/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts @@ -9,7 +9,6 @@ import { type SrcLoc, typeEquals, } from "../../ir/ir.js"; -import { keyedHelperName } from "./stable-names.js"; import { numLit, varRef } from "../../ir/build.js"; import { typeKey } from "../type-mapper.js"; import type { Lowerer } from "./lowerer.js"; @@ -126,7 +125,7 @@ export function lowerArrayFill( writeNull; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%arr.fill", key); + name = "%arr.fill." + lowerer.arrHofHelpers.size; lowerer.arrHofHelpers.set(key, name); const a = varRef("a.0", arrType, loc); const n = varRef("n.0", F64, loc); diff --git a/packages/compiler/src/frontend/lowering/array-values.ts b/packages/compiler/src/frontend/lowering/array-values.ts index 314cdfd243..0ca8503df3 100644 --- a/packages/compiler/src/frontend/lowering/array-values.ts +++ b/packages/compiler/src/frontend/lowering/array-values.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { InternalCompilerError } from "../../errors.js"; import { dynUndefinedExpr, helperLoc, varRef } from "../../ir/build.js"; import { @@ -253,7 +252,7 @@ export function lowerSafeIndexRead( let name = lowerer.arrHofHelpers.get(key); if (!name) { const at = helperLoc(loc); - name = keyedHelperName(lowerer, "%arr.idxOr", key); + name = `%arr.idxOr.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const arrT = arr.type; lowerer.liftedFns.push({ @@ -299,7 +298,7 @@ export function lowerSafeBytesRead( let name = lowerer.arrHofHelpers.get(key); if (!name) { const at = helperLoc(loc); - name = keyedHelperName(lowerer, "%bytes.idxOr", key); + name = `%bytes.idxOr.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const b = varRef("b.0", bytesT, at); const i = varRef("i.0", F64, at); diff --git a/packages/compiler/src/frontend/lowering/builtins/arguments.ts b/packages/compiler/src/frontend/lowering/builtins/arguments.ts index d2fc430dd4..7f862f6031 100644 --- a/packages/compiler/src/frontend/lowering/builtins/arguments.ts +++ b/packages/compiler/src/frontend/lowering/builtins/arguments.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { BYTES_U8, BOOL, @@ -73,7 +72,7 @@ export function lowerOptionalNumberPredicate( const key = `number.optionalPredicate:${fn}:${typeKey(widened)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%number.optionalPredicate", key); + helper = `%number.optionalPredicate.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", widened, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts index d0991dce5c..5590fbe9fa 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { recordToEnvPairs } from "./environment.js"; import { lowerChildArgsArg } from "./child-process.js"; import { boolLit, numLit, varRef } from "../../../ir/build.js"; @@ -219,7 +218,7 @@ function execFileAsyncHelper( const key = shell ? "execAsync" : "execFileAsync"; const existing = lowerer.valueHelpers.get(key); if (existing) return { name: existing, shapeId, envShapeId }; - const name = keyedHelperName(lowerer, `%${key}`, key); + const name = `%${key}.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; const envRecT: IrType = { kind: "record", shapeId: envShapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts index b9858e7681..24adcc1ba1 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { recordToEnvPairs } from "./environment.js"; import { lowerChildArgsArg } from "./child-process.js"; import { boolLit, numLit, strLit, varRef } from "../../../ir/build.js"; @@ -542,7 +541,7 @@ function execStdioModeHelper( const key = `cp.stdiomode:${shapeId}:${fd}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%cp.stdioMode.${fd}`, key); + const name = `%cp.stdioMode.${fd}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const optsT: IrType = { kind: "record", shapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts index 375c3f1e72..1b9a9c03a8 100644 --- a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts +++ b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; @@ -85,7 +84,7 @@ export function lowerDisposableStackUseCall( const key = `disposableStack.use:${className}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%disposableStack.use", key); + helper = `%disposableStack.use.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const context = newFnCtx(false, null, null, VOID); lowerer.fnStack.push(context); diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index 6b988c491d..0e1c062c50 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { dynUndefinedExpr, boolLit, numLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, ladderFenceExpr } from "../lowerer.js"; @@ -443,7 +442,7 @@ export function lowerStringOrBytesWrite( const key = `fs.${append ? "append" : "write"}:${dataType.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%fs.writeData", key); + helper = `%fs.writeData.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const body: IrStmt[] = []; for (let tag = 0; tag < arms.length; tag++) { diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index 64f829865d..7eb8aa9b2c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { isDateJsonSource } from "./dates.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; @@ -254,7 +253,7 @@ function lowerOptionalStringifyRoot( const key = `json.optionalString:${value.type.unionId}:${JSON.stringify(indent)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%json.optionalString", key); + helper = `%json.optionalString.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", value.type, loc); const serialized: IrExpr = { diff --git a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts index ee189a1f35..c104650393 100644 --- a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts +++ b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { numLit, strLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; @@ -220,7 +219,7 @@ export function lowerOsCpusCall(lowerer: Lowerer, call: ts.CallExpression, loc: const key = `os.cpus:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%os.cpus", key); + helper = `%os.cpus.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const index = varRef("i.0", F64, loc); const field = (k: number): IrExpr => ({ @@ -347,7 +346,7 @@ export function lowerPathParseCall( const key = `${fn}:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%path.parse", key); + helper = `%path.parse.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const parts = varRef("parts.0", arrayOf(STRING), loc); lowerer.liftedFns.push({ @@ -449,7 +448,7 @@ export function lowerPathFormatCall( const key = `${fn}:${typeKey(recordT)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%path.format", key); + helper = `%path.format.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const object = varRef("object.0", recordT, loc); const read = (name: string): IrExpr => { diff --git a/packages/compiler/src/frontend/lowering/builtins/performance.ts b/packages/compiler/src/frontend/lowering/builtins/performance.ts index bae525c17c..72304e6a90 100644 --- a/packages/compiler/src/frontend/lowering/builtins/performance.ts +++ b/packages/compiler/src/frontend/lowering/builtins/performance.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; @@ -331,7 +330,7 @@ function lowerPerfTimelineCall( const key = `perf.${many ? "entries" : timeline.fn}:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%perf.${many ? "entries" : member}`, key); + helper = `%perf.${many ? "entries" : member}.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const argsRef = varRef("a.0", DYN, loc); const whichRef = varRef("w.0", F64, loc); @@ -462,7 +461,7 @@ export function perfEntryTag( const key = `perf.tag:${entryT.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%perf.tag", key); + helper = `%perf.tag.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const e = varRef("e.0", entryT, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 842f42b579..25653f5419 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { stdinKeypressListener } from "./readline.js"; import { envSnapshotHelper, lowerProcessLoadEnvFile } from "./environment.js"; import { dynUndefinedExpr, boolLit, numLit, strLit, varRef } from "../../../ir/build.js"; @@ -1562,7 +1561,7 @@ export function lowerProcessMethodCall( const key = `${prefix}usage.diff:${prevT.shapeId}:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%${prefix}usage.diff`, key); + helper = `%${prefix}usage.diff.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const pRef: IrExpr = { kind: "varRef", localId: "p.0", type: prevT, loc }; const fieldOf = (name: string): IrExpr => ({ @@ -1637,7 +1636,7 @@ export function lowerProcessMethodCall( const key = `process.memoryUsage:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%process.memoryUsage", key); + helper = `%process.memoryUsage.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); lowerer.liftedFns.push({ name: helper, diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 75e527aa5d..668ef01f8c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { checkedPromiseAll, thenReader } from "../checked-promise-all.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -56,7 +55,7 @@ function lowerTuplePromiseAll( const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(resultT.inner)}`; let name = lowerer.arrHofHelpers.get(`promise.allTuple:${key}`); if (name === undefined) { - name = keyedHelperName(lowerer, "%promise.allTuple", `promise.allTuple:${key}`); + name = `%promise.allTuple.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(`promise.allTuple:${key}`, name); const ctx = newFnCtx(false, null, null, resultT.inner); ctx.isAsync = true; @@ -558,7 +557,7 @@ export function lowerPromiseStaticCall( const key = `promise.try:${typeKey(f.type)}:${args.map((a) => typeKey(a.type)).join(",")}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%promise.try", key); + helper = `%promise.try.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const fRef: IrExpr = { kind: "varRef", localId: "f.0", type: f.type, loc }; const argLocals = args.map((a, i) => ({ id: `a${i}.0`, name: `a${i}`, type: a.type })); diff --git a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts index b4dbbf5137..822a4fecc3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { varRef } from "../../../ir/build.js"; @@ -171,7 +170,7 @@ function promisifyMaker(lowerer: Lowerer, shape: CallbackShape, loc: SrcLoc): st const key = `promisify:${typeKey(shape.fnT)}=>${typeKey(shape.resultT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const base = keyedHelperName(lowerer, "%promisify", key); + const base = `%promisify.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, base); const { fnT, resultT, args, callbackT, callbackParamT, inner, error } = shape; const promiseT = resultT.ret; diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index 8d47c8e3af..7229d5b84f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { isBuiltinClassInstance } from "./receiver-types.js"; import { strLit, varRef, dynUndefinedExpr } from "../../../ir/build.js"; import { staticTextDecoderEncoding } from "../text-decoder-encoding.js"; @@ -130,7 +129,7 @@ function stringDecoderHelper( const key = `strdec.${op}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%strdec.${op}`, key); + const name = `%strdec.${op}.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts index d7cbd63b50..2d531663f3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/timers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts @@ -85,7 +85,7 @@ export function lowerTimersPromisesSetInterval( const genT = timersPromisesIntervalType(lowerer, expr); if (genT === null) lowerer.badType(expr, lowerer.typeOf(expr)); const yielded = lowerer.coerceInto(valueNode, value, genT.yieldT); - const fnName = lowerer.closureName(loc, "_tpInterval"); + const fnName = `%fn${lowerer.lambdaCounter++}_tpInterval`; const msParam: IrLocal = { id: "%tp.ms", name: "delay", type: F64, mutable: false }; const valueParam: IrLocal = { id: "%tp.value", name: "value", type: genT.yieldT, mutable: false }; const promiseVoid: IrType = { kind: "promise", inner: VOID }; diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts index 2a77fa85c1..dca8a7cc00 100644 --- a/packages/compiler/src/frontend/lowering/builtins/url.ts +++ b/packages/compiler/src/frontend/lowering/builtins/url.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { nodeThrowExpr, countedFor, strLit, varRef, dynUndefinedExpr } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; @@ -293,7 +292,7 @@ function searchParamsFromTuples( const key = `sp.fromTuples:${row.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%sp.fromTuples", key); + helper = `%sp.fromTuples.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const pairsT = init.type; const sp = (): IrExpr => varRef("sp.0", SEARCH_PARAMS_T, loc); @@ -373,7 +372,7 @@ function lowerOptionalStringSearchParams( const key = `sp.optionalString:${init.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%sp.optionalString", key); + helper = `%sp.optionalString.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const value = varRef("init.0", init.type, loc); lowerer.liftedFns.push({ @@ -679,7 +678,7 @@ function lowerSpForEachCall( const key = `${arity}:${typeKey(fnRet)}`; let helper = lowerer.spHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%sp.forEach", key); + helper = `%sp.forEach.${lowerer.spHofHelpers.size}`; lowerer.spHofHelpers.set(key, helper); lowerer.liftedFns.push(buildSpForEachFn(helper, arity, fnRet, loc)); } diff --git a/packages/compiler/src/frontend/lowering/builtins/v8.ts b/packages/compiler/src/frontend/lowering/builtins/v8.ts index 989a61a3f0..3582ce8aeb 100644 --- a/packages/compiler/src/frontend/lowering/builtins/v8.ts +++ b/packages/compiler/src/frontend/lowering/builtins/v8.ts @@ -5,7 +5,6 @@ * spirit of process.memoryUsage(): approximate values, not V8 heap * figures (a compiled binary embeds no V8 engine). Every other member * fences at its use with the by-design reason (builtinFenceHintOf). */ -import { keyedHelperName } from "../stable-names.js"; import { numLit } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; @@ -56,7 +55,7 @@ export function lowerV8HeapStatisticsCall( const key = `v8.getHeapStatistics:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%v8.getHeapStatistics", key); + helper = `%v8.getHeapStatistics.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const sample = (index: number): IrExpr => ({ kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index 060ffe9d24..49f7c8ec0f 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { forwardedTupleType, pureForwardedValues, stripTypeWrappers } from "./typed-arguments.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; @@ -754,7 +753,7 @@ function holeReadingLift(lowerer: Lowerer, from: IrType, to: IrType, loc: SrcLoc const key = `arr.holes:${typeKey(elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%arr.holes", key); + const name = `%arr.holes.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push( buildArrayConversion( diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index dacb4d3246..34343ad270 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -849,10 +849,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara lowerer.genericFnsBySymbol.set(symbol, { decl, baseName: nameText, - qualifiedName: lowerer.qualify( - ts.getSourceFileOfNode(decl), - nsPathPrefix(lowerer, decl) + nameText, - ), + qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText), typeParams: [], instances: new Map(), implicitParams: implicit, @@ -900,7 +897,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara // "%default" spelling ('%' cannot appear in a user identifier). name: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(lowerer, decl) + (decl.name?.text ?? "%default"), + nsPathPrefix(decl) + (decl.name?.text ?? "%default"), ), params, returnType, diff --git a/packages/compiler/src/frontend/lowering/class-assertions.ts b/packages/compiler/src/frontend/lowering/class-assertions.ts index 52654fab8a..900c324262 100644 --- a/packages/compiler/src/frontend/lowering/class-assertions.ts +++ b/packages/compiler/src/frontend/lowering/class-assertions.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { nodeThrowExpr, varRef } from "../../ir/build.js"; import { BOOL, @@ -123,7 +122,7 @@ export function checkedClassAssertion( const key = `classassert:${typeKey(source.type)}:${typeKey(target)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = keyedHelperName(lowerer, "%class.assert", key); + name = `%class.assert.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); const input = varRef("value.0", source.type, loc); const body: IrStmt[] = []; @@ -229,7 +228,7 @@ export function checkedClassUnionAssertion( const key = `classunionassert:${typeKey(value.type)}:${typeKey(target)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = keyedHelperName(lowerer, "%class.assert", key); + name = `%class.assert.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); const input = varRef("value.0", value.type, loc); const locals = [{ id: "value.0", name: "value", type: value.type, mutable: false }]; diff --git a/packages/compiler/src/frontend/lowering/class-unions.ts b/packages/compiler/src/frontend/lowering/class-unions.ts index 0021b2cf89..f52026ba42 100644 --- a/packages/compiler/src/frontend/lowering/class-unions.ts +++ b/packages/compiler/src/frontend/lowering/class-unions.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { BOOL, typeKey, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; import { nodeThrowExpr, varRef } from "../../ir/build.js"; @@ -133,7 +132,7 @@ export function narrowClassUnion( const key = `classUnion:${source.className}:${target.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%class.union", key); + helper = `%class.union.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", source, loc); // Rank by ancestry before sorting: returning zero for unrelated pairs diff --git a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts index fe3c14605c..ffdac19f0d 100644 --- a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts +++ b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; import { buildFunctionAdapter } from "./builders.js"; import { InternalCompilerError } from "../../../errors.js"; @@ -34,7 +33,7 @@ export function funcReturnWidthAdapter( const key = `fn:${typeKey(fromT.ret.elem)}:${typeKey(toT.ret.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%fn.width", key); + const name = `%fn.width.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); // The returned closure's body: call the captured original, width-map. @@ -164,7 +163,7 @@ function restPackingAdapter( const key = `fnrestadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%fn.adapt", key); + const name = `%fn.adapt.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const fixed = fromT.params.length - 1; @@ -221,7 +220,7 @@ function funcIntoDynRestAdapter( const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%fn.adapt", key); + const name = `%fn.adapt.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = [{ localId: "rest.0", name: "args", type: DYN }]; @@ -377,7 +376,7 @@ export function funcCoerceAdapter( const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%fn.adapt", key); + const name = `%fn.adapt.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = toT.params.map((t, i) => ({ @@ -605,7 +604,7 @@ export function spawnResFnAdapter( const key = `fnspawn:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%fnval.spawnres", key); + const name = `%fnval.spawnres.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = toT.params.map((p, i) => ({ diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 5b5e283b32..496a296074 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { constructedValue } from "../constructor-returns.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import type { FieldLift } from "./structural-plans.js"; @@ -254,7 +253,7 @@ export function unionRecordWidthHelper( const key = `unionWidth:${fromId}:${target.shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%union.record", key); + const name = `%union.record.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const type: IrType = { kind: "union", unionId: fromId }; const value = varRef("value.0", type, loc); @@ -313,7 +312,7 @@ export function recordWidthHelper( const key = `rec:${fromId}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%rec.width", key); + const name = `%rec.width.${lowerer.valueHelpers.size}`; // Interned BEFORE the body builds: a recursive nested-width field // (self-referential shapes) resolves to this helper itself. lowerer.valueHelpers.set(key, name); @@ -342,7 +341,7 @@ export function tupleArrayWidthHelper( const key = `tuparr:${fromId}:${typeKey(toT.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%tup.arr", key); + const name = `%tup.arr.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "record", shapeId: fromId }; const t: IrExpr = { kind: "varRef", localId: "t.0", type: fromT, loc }; @@ -389,7 +388,7 @@ export function emptyArrayLiftHelper( const key = `emptyarr:${typeKey(fromT.elem)}:${typeKey(toT.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%arr.empty", key); + const name = `%arr.empty.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const a: IrExpr = { kind: "varRef", localId: "a.0", type: fromT, loc }; lowerer.liftedFns.push({ @@ -453,7 +452,7 @@ export function arrayWidthHelper( const key = `arr:${typeKey(fromElem)}:${typeKey(toElem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%arr.width", key); + const name = `%arr.width.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const arrT: IrType = { kind: "array", elem: fromElem }; const outT: IrType = { kind: "array", elem: toElem }; @@ -493,7 +492,7 @@ export function objRecordWidthHelper( const key = `obj:${className}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%obj.width", key); + const name = `%obj.width.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "object", className }; lowerer.liftedFns.push(buildRecordProjection(lowerer, name, fromT, to, plan, loc)); @@ -518,7 +517,7 @@ export function recordClassWidthHelper( const key = `cls:${fromId}:${className}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%cls.width", key); + const name = `%cls.width.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.noteEdge(`%${className}.constructor`); const fromT: IrType = { kind: "record", shapeId: fromId }; diff --git a/packages/compiler/src/frontend/lowering/coercions/unions.ts b/packages/compiler/src/frontend/lowering/coercions/unions.ts index 434e687a50..06c0af9c6b 100644 --- a/packages/compiler/src/frontend/lowering/coercions/unions.ts +++ b/packages/compiler/src/frontend/lowering/coercions/unions.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { buildUnionNarrow, unionNarrowFailureMessage } from "../union-narrow.js"; import { isSafeToRepeat } from "../expressions/evaluation-safety.js"; import { nodeThrowExpr } from "../../../ir/build.js"; @@ -148,7 +147,7 @@ export function strandedUnitTrap( const key = `strandunit:${typeKey(expected)}:${expr.type.kind}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = keyedHelperName(lowerer, "%unit.strand", key); + name = `%unit.strand.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push({ name, @@ -228,7 +227,7 @@ export function strandedCoercionTrap( const key = `strand:${expected.unionId}:${typeKey(src)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = keyedHelperName(lowerer, "%union.strand", key); + name = `%union.strand.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); const toT: IrType = { kind: "union", unionId: expected.unionId }; lowerer.liftedFns.push({ @@ -276,7 +275,7 @@ export function recordUnionWrapHelper( const key = `recordWrap:${source.shapeId}:${toId}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%record.wrap", key); + const name = `%record.wrap.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) => @@ -308,7 +307,7 @@ export function recordUnionRouteHelper( (from, dst) => lowerer.widthLiftPlan(from, dst), ); if (!plan) return null; - const name = keyedHelperName(lowerer, "%record.route", key); + const name = `%record.route.${lowerer.coercions.retags.size}`; lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) => @@ -353,7 +352,7 @@ export function unionRetagHelper( }); const key = `${fromId}:${toId}:${stranded.join(".")}`; const existing = lowerer.coercions.retags.get(key); - const name = existing ?? keyedHelperName(lowerer, "%union.retag", key); + const name = existing ?? `%union.retag.${lowerer.coercions.retags.size}`; if (!existing) { lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( @@ -411,7 +410,7 @@ export function narrowedArmHelper( lowerer.coercions.checkedNarrows.add(existing); return existing; } - const name = keyedHelperName(lowerer, "%union.narrow", key); + const name = `%union.narrow.${lowerer.coercions.narrows.size}`; lowerer.coercions.narrows.set(key, name); lowerer.coercions.checkedNarrows.add(name); const fn = buildUnionNarrow(name, from, target, loc, (type) => lowerer.fmt(type)); @@ -588,7 +587,7 @@ export function deferredReadHelper( const key = `deferred:${fromId}:${tag}`; const existing = lowerer.coercions.narrows.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%deferred.read", key); + const name = `%deferred.read.${lowerer.coercions.narrows.size}`; lowerer.coercions.narrows.set(key, name); const dflt: IrExpr = target.kind === "bool" diff --git a/packages/compiler/src/frontend/lowering/collection-ingestion.ts b/packages/compiler/src/frontend/lowering/collection-ingestion.ts index 8fe2a6bbbf..5d14d54925 100644 --- a/packages/compiler/src/frontend/lowering/collection-ingestion.ts +++ b/packages/compiler/src/frontend/lowering/collection-ingestion.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { InternalCompilerError } from "../../errors.js"; import { typeKey } from "../type-mapper.js"; @@ -198,7 +197,7 @@ export function ingestCollection( const key = `ingest:${input.collection ? typeKey(input.collection) + ":" + input.selection : "cursor:" + typeKey(input.element)}:${typeKey(destination)}:${mapper ? typeKey(mapper.type) : "copy"}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%collection.ingest", key); + name = `%collection.ingest.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const context = newFnCtx(false, null, null, destination); lowerer.fnStack.push(context); diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 7ca69d9ba3..eb99592bb9 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js"; import { transformStmtList } from "../../../ir/traverse.js"; import type { ScrDiagnostic } from "../../../diagnostics/diagnostic.js"; @@ -507,7 +506,7 @@ function arrayHofHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}:${typeKey(outElem)}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%arr.${method}`, key); + const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method, elem, fnRet, arity, outElem, present, split: !!fastT }); const fn = buildArrayHofFn( @@ -620,7 +619,7 @@ function arrayUnionHofHelper( const key = `union:${method}:${receiverT.unionId}:${typeKey(callbackT)}:${typeKey(resultT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%arr.union", key); + const name = `%arr.union.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const receiver = varRef("a.0", receiverT, loc); const callback = varRef("f.0", callbackT, loc); @@ -1205,7 +1204,7 @@ function findHelper( const key = `${method}:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}${site}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%arr.${method}`, key); + const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); const valueT = callbackValueType(lowerer, elem, present); @@ -1305,7 +1304,7 @@ function findIndexHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%arr.${method}`, key); + const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); if (!last) lowerer.arrHofInfo.set(name, { @@ -1389,7 +1388,7 @@ function someEveryHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%arr.${method}`, key); + const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method, @@ -1588,7 +1587,7 @@ function flatMapHelper( const key = `flatMap:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%arr.flatMap", key); + const name = `%arr.flatMap.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method: "flatMapArray", @@ -1830,7 +1829,7 @@ function reduceHelper( const key = `${method}:${typeKey(elem)}:${typeKey(accT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : "seed"}${site}${fastT ? ":split" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%arr.${method}`, key); + const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); if (method === "reduce") lowerer.arrHofInfo.set(name, { @@ -2122,7 +2121,7 @@ export function lowerArraySortCall( const key = `${method}:${typeKey(elem)}:${arity}${native ? ":native" : ""}${keyed ? `:keyed${order}${reads ? `:field${reads.length}` : ""}` : ""}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%arr.${method}`, key); + helper = `%arr.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const undefinedTag = elem.kind === "union" ? lowerer.armTag(elem.unionId, UNDEFINED_T) : -1; lowerer.liftedFns.push( @@ -2168,7 +2167,7 @@ function sortKeyReader( const cacheKey = `sortKey:${typeKey(elem)}:${sortKeysId(reads.map((read) => ({ order: 1, read })))}`; let reader = lowerer.arrHofHelpers.get(cacheKey); if (!reader) { - reader = keyedHelperName(lowerer, "%arr.sortKey", cacheKey); + reader = `%arr.sortKey.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(cacheKey, reader); lowerer.liftedFns.push(buildSortKeyFn(reader, elem, reads, loc)); } @@ -2309,7 +2308,7 @@ function dynFlatMapHelper(lowerer: Lowerer, elemT: IrType, arity: number, loc: S const key = `dynflatmap:${typeKey(elemT)}:${arity}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%dyn.flatmap", key); + const name = `%dyn.flatmap.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push(buildDynFlatMapFn(name, elemT, arity, loc)); return name; @@ -2482,7 +2481,7 @@ function dynFilterHelper(lowerer: Lowerer, elemT: IrType, arity: number, loc: Sr const key = `dynfilter:${typeKey(elemT)}:${arity}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%dyn.filter", key); + const name = `%dyn.filter.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push(buildDynFilterFn(name, elemT, arity, loc)); return name; @@ -2677,7 +2676,7 @@ export function lowerTuplePredicateCall( const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(fnArg.type)}:${typeKey(resultT)}:${arity}${callbackSite(fnArg)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%tuple.${method}`, key); + helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const v = varRef("v.0", elem, loc); const out = varRef("out.0", resultT, loc); diff --git a/packages/compiler/src/frontend/lowering/containers/array-concat.ts b/packages/compiler/src/frontend/lowering/containers/array-concat.ts index b4e285ee98..4f6e4ca437 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-concat.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-concat.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { identityPreservingWidening } from "../coercions/identity.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import { @@ -29,7 +28,7 @@ export function arrayConcatHelper( const key = `concat:${typeKey(elem)}:${shape.map((type) => (type ? `a:${typeKey(type)}` : "e")).join(";")}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%arr.concat", key); + const name = `%arr.concat.${lowerer.arrHofHelpers.size}`; const arrT = arrayOf(elem); const params = [ { localId: "a.0", name: "a", type: arrT }, diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 1f10640c03..40e4d64367 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { generatorDrain } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; import { liftedFreshSpread } from "../coercions/fresh-spread.js"; @@ -257,7 +256,7 @@ function appendWidenedArrayValues( const key = `insertValues:${typeKey(sourceType.elem)}:${typeKey(elem)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%arr.insertValues", key); + name = `%arr.insertValues.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const outType = arrayOf(elem); const valueType = arrayValueType(lowerer, source.type.elem); @@ -1036,7 +1035,7 @@ export function lowerArrayFromCall( const key = `fromArray:${typeKey(arrT.elem)}:${typeKey(outElem)}:${typeKey(fnRet)}:${arity}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%arr.fromArray", key); + helper = `%arr.fromArray.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push( buildArrayFromArrayFn(lowerer, helper, arrT.elem, outElem, fnRet, arity, loc), @@ -1123,7 +1122,7 @@ export function lowerArrayFromCall( const key = `fromLen:${typeKey(fnRet)}:${arity}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%arr.fromLen", key); + helper = `%arr.fromLen.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push(buildArrayFromLenFn(helper, fnRet, arity, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 44182fbda6..e9a130a2db 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { liftedFreshSpread } from "../coercions/fresh-spread.js"; import { lowerArrayIteratorValue } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; @@ -1617,7 +1616,7 @@ function arraySearchHelper( const key = `${method}:${typeKey(elem)}:${typeKey(valueT)}:${typeKey(needleT)}:${directNeedle}:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%arr.${method}`, key); + const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); const resultT = method === "includes" ? BOOL : F64; @@ -1973,7 +1972,7 @@ export function lowerTupleReadMethodCall( let helper = lowerer.arrHofHelpers.get(key); const resultType = method === "includes" ? BOOL : F64; if (!helper) { - helper = keyedHelperName(lowerer, `%tuple.${method}`, key); + helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const params = [ { localId: "a.0", name: "a", type: receiver.type }, @@ -2035,7 +2034,7 @@ export function lowerTupleReadMethodCall( const key = `tuple.slice:${typeKey(receiver.type)}:${args.length}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%tuple.slice", key); + helper = `%tuple.slice.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const params = [receiver, ...args].map((arg, i) => ({ localId: i === 0 ? "a.0" : `arg.${i}`, @@ -2126,7 +2125,7 @@ export function lowerTupleReadMethodCall( const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(fnArg.type)}:${typeKey(outElem)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%tuple.${method}`, key); + helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const out = varRef("out.0", outType, loc); const result = varRef("r.0", ret, loc); @@ -2254,7 +2253,7 @@ function lowerArrayAtCall( const key = `at:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(index.type)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%arr.at", key); + name = `%arr.at.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index 180f404351..cf45a75751 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js"; import { BOOL, @@ -852,7 +851,7 @@ class PipelineFusion { if (!built) return null; let name = this.helpers.get(built.key); if (!name) { - name = keyedHelperName(this.lowerer, "%arr.fused", built.key); + name = `%arr.fused.${this.helpers.size}`; this.helpers.set(built.key, name); this.added.push({ ...built.fn, name }); } diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 4b20d7bf55..bf9821d317 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { boolLit, dynUndefinedExpr, numLit, varRef } from "../../../ir/build.js"; import { BUF_NUM_METHODS } from "./buffer-numeric-methods.js"; import * as ts from "../../ts7/adapter.js"; @@ -78,7 +77,7 @@ function lowerBytesSortCall( const cacheKey = `bytes.${method}:${bytesT.elem}:${key}`; let helper = lowerer.arrHofHelpers.get(cacheKey); if (!helper) { - helper = keyedHelperName(lowerer, `%bytes.${method}`, cacheKey); + helper = `%bytes.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(cacheKey, helper); lowerer.liftedFns.push( buildBytesSortFn(helper, bytesT, arity, hasComparator, copyFirst, order, loc), @@ -271,7 +270,7 @@ function bytesCallbackHelper( const key = `bytes.${method}:${bytesT.elem}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%bytes.${method}`, key); + const name = `%bytes.${method}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const reduce = method === "reduce" || method === "reduceRight"; const backward = method === "reduceRight" || method === "findLast" || method === "findLastIndex"; diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index f39c8e86c7..18754f0881 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; import { isJsSourceFile, locOf } from "../../program.js"; @@ -286,7 +285,7 @@ export function truthyFilterCallback( const key = `filterBoolean:${typeKey(valueT)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%arr.filterBoolean", key); + name = `%arr.filterBoolean.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const value = varRef("v.0", valueT, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts index dae68d518a..0bf1268301 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; @@ -105,7 +104,7 @@ export function lowerGroupByStaticCall( const key = `map.groupBy:${typeKey(items.type)}:${typeKey(f.type)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%map.groupBy", key); + helper = `%map.groupBy.${lowerer.mapHofHelpers.size}`; lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildGroupByFn(lowerer, helper, items.type, f.type, resultT, loc)); } @@ -142,7 +141,7 @@ export function lowerGroupByStaticCall( const key = `object.groupBy:${typeKey(items.type)}:${typeKey(f.type)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%object.groupBy", key); + helper = `%object.groupBy.${lowerer.mapHofHelpers.size}`; lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildGroupByFn(lowerer, helper, items.type, f.type, resultT, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index d3169ec5cb..370974c85c 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { nodeThrowExpr, boolLit, countedFor, numLit, strLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -233,7 +232,7 @@ function lowerMapIterDrainCall( const key = `${method}:${typeKey(mapT.key)}:${typeKey(mapT.value)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%map.${method}`, key); + helper = `%map.${method}.${lowerer.mapHofHelpers.size}`; lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildMapIterDrainFn(helper, mapT, method, elemT, tupleT, loc)); } @@ -349,7 +348,7 @@ export function mapEntriesArray(lowerer: Lowerer, receiver: IrExpr): IrExpr { const key = `entries:${typeKey(mapT.key)}:${typeKey(mapT.value)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%map.entries", key); + helper = `%map.entries.${lowerer.mapHofHelpers.size}`; lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push( buildMapIterDrainFn(helper, mapT, "entries", tupleT, tupleT, receiver.loc), @@ -520,7 +519,7 @@ export function lowerMapForEachCall( const key = `${typeKey(mapT.key)}:${typeKey(mapT.value)}:${arity}:${typeKey(fnRet)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%map.forEach", key); + helper = `%map.forEach.${lowerer.mapHofHelpers.size}`; lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildCollectionForEachFn(helper, mapT, arity, fnRet, loc)); } @@ -694,7 +693,7 @@ export function lowerSetForEachCall( const key = `${typeKey(setT.elem)}:${arity}:${typeKey(fnRet)}`; let helper = lowerer.setHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%set.forEach", key); + helper = `%set.forEach.${lowerer.setHofHelpers.size}`; lowerer.setHofHelpers.set(key, helper); lowerer.liftedFns.push(buildCollectionForEachFn(helper, setT, arity, fnRet, loc)); } @@ -746,7 +745,7 @@ function lowerSetCombineCall( const key = `${name}:${typeKey(setT.elem)}:${typeKey(otherT)}`; let helper = lowerer.setHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%set.${name}`, key); + helper = `%set.${name}.${lowerer.setHofHelpers.size}`; lowerer.setHofHelpers.set(key, helper); lowerer.liftedFns.push(buildSetCombineFn(helper, name, setT, otherT, loc)); } @@ -1358,7 +1357,7 @@ export function mapFromSeedValue( const key = `checked-seed:${typeKey(mapT)}`; let name = lowerer.mapHofHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%map.checkedSeed", key); + name = `%map.checkedSeed.${lowerer.mapHofHelpers.size}`; lowerer.mapHofHelpers.set(key, name); const source = varRef("source.0", DYN, loc); const entries = varRef("entries.0", DYN, loc); @@ -1458,7 +1457,7 @@ export function mapFromSeedValue( const key = `seed:${typeKey(mapT.key)}:${typeKey(mapT.value)}:${elem.shapeId}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%map.seed", key); + helper = `%map.seed.${lowerer.mapHofHelpers.size}`; lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildMapSeedFn(mapT, helper, seed.type, elem, kT, vT, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index b3faf649eb..efc173ffca 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import { objectEnumerationReceiver } from "../object-enumeration-receiver.js"; @@ -185,7 +184,7 @@ export function objectIterOverIndexShape( const key = `obj.${member}:ovf:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%obj.${member}`, key); + helper = `%obj.${member}.${lowerer.arrHofHelpers.size}`; const outRef = varRef("out.0", resultT, loc); const rRef = varRef("r.0", argIr, loc); @@ -609,7 +608,7 @@ function lowerTypedObjectFromEntriesCall( const key = `obj.fromEntries:${argIr.elem.shapeId}:${resultT.shapeId}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%obj.fromEntries", key); + helper = `%obj.fromEntries.${lowerer.arrHofHelpers.size}`; const tupleT = argIr.elem as IrType & { kind: "record" }; const tRef = varRef("t.0", tupleT, loc); @@ -713,7 +712,7 @@ function lowerFromEntriesStringRows( const key = `obj.fromEntries:strrows:${resultT.shapeId}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%obj.fromEntries", key); + helper = `%obj.fromEntries.${lowerer.arrHofHelpers.size}`; const tRef = varRef("t.0", rowT, loc); const rowLen: IrExpr = { @@ -932,7 +931,7 @@ export function lowerRecordOvfCaptureHelper( const key = `ovf:${fromId}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%rec.capture", key); + const name = `%rec.capture.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "record", shapeId: fromId }; const toT: IrType = { kind: "record", shapeId: toId }; @@ -1205,7 +1204,7 @@ export function lowerObjectAssignIndexShape( const key = `assign:${targetIr.shapeId}:${plan.fromId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%obj.assign", key); + const name = `%obj.assign.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const toT: IrType = { kind: "record", shapeId: targetIr.shapeId }; const fromT: IrType = { kind: "record", shapeId: plan.fromId }; @@ -1475,7 +1474,7 @@ export function lowerIndexMergeHelper( .join(","); const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%rec.merge", key); + const name = `%rec.merge.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const toT: IrType = { kind: "record", shapeId: toId }; const ksT = arrayOf(STRING); @@ -1806,7 +1805,7 @@ export function lowerEnvToPairsHelper( const key = `env.pairs:${shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%env.pairs", key); + const name = `%env.pairs.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; const arrT = arrayOf(STRING); diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index 48b4caabdb..c23e6931c6 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { boolLit, numLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -967,7 +966,7 @@ function lowerFusedChain( const key = `iter:${srcKey}:${stageKeys.join(",")}:${terminal}:${termArgs.map((a) => typeKey(a.type)).join(",")}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%iter.${terminal}`, key); + helper = `%iter.${terminal}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push( buildIterChainFn( @@ -1039,7 +1038,7 @@ function lowerHelperObject( const key = `iterHelper:${stage.kind}:${srcKey}:${typeKey(srcElem)}:${typeKey(arg.type)}:${typeKey(outT)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, `%iter.${stage.kind}`, key); + name = `%iter.${stage.kind}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( buildHelperGenFn(lowerer, name, source, srcElem, stage, arg.type, outT, resultType, loc), @@ -1059,7 +1058,7 @@ function internBudgetChecker(lowerer: Lowerer, loc: SrcLoc, srcT: GenType | null const key = srcT === null ? "iter:budget" : `iter:budget:close:${typeKey(srcT)}`; let name = lowerer.arrHofHelpers.get(key); if (name !== undefined) return name; - name = srcT === null ? "%iter.budget" : keyedHelperName(lowerer, "%iter.budget.close", key); + name = srcT === null ? "%iter.budget" : `%iter.budget.close.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const v = (): IrExpr => ({ kind: "varRef", localId: "v.0", type: F64, loc }); diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index fe4ddbc6c3..9567ee0f6c 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { matchAllRowsReadByElements } from "./match-row-uses.js"; import { boolLit, @@ -160,7 +159,7 @@ export function lowerStringSplitCall( const key = `str.split:${receiver.type.kind === "union" ? typeKey(receiver.type) : receiver.type.kind}:${typeKey(separator.type)}:${typeKey(limit.type)}:${absent}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%str.split", key); + helper = `%str.split.${lowerer.valueHelpers.size}`; const values = [receiver, separator, limit]; const params = values.map((value, index) => ({ localId: `arg.${index}`, @@ -396,7 +395,7 @@ function stringIndexHelperCall( const key = `str.index:${method}:${typeKey(receiver.type)}:${typeKey(index.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%str.index", key); + helper = `%str.index.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const rawReceiver = varRef("arg.0", receiver.type, loc); const rawIndex = varRef("arg.1", index.type, loc); @@ -674,7 +673,7 @@ export function lowerStringPaddingCall( const key = `str.pad:${method}:${values.map((value) => typeKey(value.type)).join(":")}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%str.pad", key); + helper = `%str.pad.${lowerer.valueHelpers.size}`; const params = values.map((value, index) => ({ localId: `arg.${index}`, name: `arg${index}`, @@ -1310,7 +1309,7 @@ export function lowerStringMethodCall( const key = `str.positions:${entry.method}:${typeKey(needle.type)}:${typeKey(position.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%str.positions", key); + helper = `%str.positions.${lowerer.valueHelpers.size}`; const params = [receiver, needle, position].map((arg, index) => ({ localId: `arg.${index}`, name: `arg${index}`, @@ -1412,7 +1411,7 @@ export function lowerStringMethodCall( const key = `str.positions:${entry.method}:${args.map((arg) => typeKey(arg.type)).join(":")}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%str.positions", key); + helper = `%str.positions.${lowerer.valueHelpers.size}`; const params = [receiver, ...args].map((arg, index) => ({ localId: `arg.${index}`, name: `arg${index}`, diff --git a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts index 780c77d0f0..6856bc50bb 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { BOOL, @@ -163,7 +162,7 @@ function lowerStringReplacementValues( const key = `str.${method}:${typeKey(replacement.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%str.${method}`, key); + helper = `%str.${method}.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const subject = varRef("s.0", STRING, loc); const needle = varRef("search.0", STRING, loc); diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 7e449f12b7..fafbbf695a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -1,5 +1,4 @@ /* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */ -import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { cleanFuncAdaptable } from "../coercions/function-adapters.js"; import { type Lowerer, jsFuncNameOf } from "../lowerer.js"; @@ -802,7 +801,7 @@ function nullProtoToString(lowerer: Lowerer, e: IrExpr, t: IrType & { kind: "rec const key = `nullProtoToString:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%rec.tostr", key); + helper = `%rec.tostr.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const r = varRef("r.0", t, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 93bf43e953..565db0cd8d 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "../stable-names.js"; import { dateJsonType, isDateJsonSource } from "../builtins/dates.js"; import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js"; import { pathModuleValue } from "../lower-builtin-values.js"; @@ -407,7 +406,7 @@ export function lowerDynObjectLiteral( if (!(err instanceof PoisonError) || !pureMember) throw err; const fence = lowerer.deferToRuntimeFence(propDiagsBefore, prop, { kind: "closure", - name: () => lowerer.closureName(loc, "_dynfence"), + name: () => `%fn${lowerer.lambdaCounter++}_dynfence`, returnType: VOID, type: { kind: "func", params: [], ret: VOID }, }); @@ -462,7 +461,7 @@ export function lowerDynObjectLiteral( if (!(err instanceof PoisonError)) throw err; const fence = lowerer.deferToRuntimeFence(convDiagsBefore, prop, { kind: "closure", - name: () => lowerer.closureName(loc, "_dynfence"), + name: () => `%fn${lowerer.lambdaCounter++}_dynfence`, returnType: VOID, type: { kind: "func", params: [], ret: VOID }, }); @@ -2957,7 +2956,7 @@ function lowerDeclaredSpreadMerge( const key = `declmerge-source:${type.shapeId}:${source.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%rec.declmerge", key); + helper = `%rec.declmerge.${lowerer.valueHelpers.size}`; const sourceType = source.value.type; const destination = varRef("out.0", type, loc); const receiver = varRef("source.0", sourceType, loc); diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 4b8c5692a4..fbcecdc328 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -240,7 +240,7 @@ export function lowerOptionalChain( const receiver = lowerer.runtimeOptionalSourceValue(recvNode, loweredReceiver) ?? loweredReceiver; if (receiver.type.kind === "dyn") { if (dotNode === expr && ts.isCallExpression(expr)) { - const id = lowerer.closureName(loc, "", "chain"); + const id = `chain.${lowerer.chainCounter++}`; const callee: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; const stmts: IrStmt[] = []; let guarded = receiver; @@ -298,7 +298,7 @@ export function lowerOptionalChain( ); } if (dotNode === expr && ts.isElementAccessExpression(expr)) { - const id = lowerer.closureName(loc, "", "chain"); + const id = `chain.${lowerer.chainCounter++}`; const reference: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; lowerer.chainRecvByNode.set(recvNode, reference); lowerer.chainHandled.add(dotNode); @@ -331,7 +331,7 @@ export function lowerOptionalChain( dotNode === expr.expression ) { const m = dotNode.name.text; - const id = lowerer.closureName(loc, "", "chain"); + const id = `chain.${lowerer.chainCounter++}`; const recvRef: IrExpr = { kind: "chainRecv", id, type: DYN, loc: locOf(recvNode) }; lowerer.chainRecvByNode.set(recvNode, recvRef); lowerer.chainHandled.add(dotNode); @@ -358,7 +358,7 @@ export function lowerOptionalChain( } return { kind: "optChain", id, receiver, body, type: DYN, loc }; } - const id = lowerer.closureName(loc, "", "chain"); + const id = `chain.${lowerer.chainCounter++}`; const reference: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; lowerer.chainRecvByNode.set(recvNode, reference); lowerer.chainHandled.add(dotNode); @@ -420,7 +420,7 @@ export function lowerOptionalChain( rest.length === 1 ? rest[0]! : { kind: "union" as const, unionId: lowerer.unions.transform(def, rest) }; - const id = lowerer.closureName(loc, "", "chain"); + const id = `chain.${lowerer.chainCounter++}`; // A single present arm binds its payload. Multiple present arms bind // the original tagged value, then retag inside the guarded body. The // nullish arms never enter that body, and each record keeps its identity. diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 6c8dfa3bf0..f0b980da05 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1,5 +1,4 @@ /* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */ -import { keyedHelperName } from "../stable-names.js"; import { staticallySelected } from "../static-typeof.js"; import * as ts from "../../ts7/adapter.js"; import { lowerHistogramProperty } from "../builtins/performance.js"; @@ -2784,7 +2783,7 @@ export function lowerGroupsProjection( const key = `regexgroups:${shapeId}:${groups.map((g) => `${g.name}=${g.index}`).join(",")}:${recvKey}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%regex.groups", key); + helper = `%regex.groups.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const body: IrStmt[] = []; const locals: IrLocal[] = [{ id: "m.0", name: "m", type: recv.type, mutable: true }]; diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 8363918352..088b45cffa 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -1,5 +1,4 @@ /* instanceof and in, including private-name and runtime-key in. */ -import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { builtinClassInstanceOfAny, builtinClassKind } from "../builtins/builtin-class-dyn.js"; import { @@ -648,7 +647,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const key = `instanceof.union:${unionId}:${target.def.name}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%instanceof.union", key); + helper = `%instanceof.union.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const value: IrExpr = { kind: "varRef", localId: "value.0", type: left.type, loc }; const body: IrStmt[] = []; @@ -1848,7 +1847,7 @@ function lowerRuntimeKeyIn( const hkey = `haskey:${recvT.shapeId}`; let helper = lowerer.valueHelpers.get(hkey); if (!helper) { - helper = keyedHelperName(lowerer, "%rec.haskey", hkey); + helper = `%rec.haskey.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(hkey, helper); const recT: IrType = { kind: "record", shapeId: recvT.shapeId }; diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index daf082db73..abcd5a584a 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -1,6 +1,4 @@ import { staticNameBindings } from "./static-function-names.js"; -import { positionTag } from "./structural-positions.js"; -import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import type { Lowerer } from "./lowerer.js"; @@ -231,10 +229,7 @@ export function collectGenericSignature(lowerer: Lowerer, decl: ts.FunctionDecla lowerer.genericFnsBySymbol.set(symbol, { decl, baseName: nameText, - qualifiedName: lowerer.qualify( - ts.getSourceFileOfNode(decl), - nsPathPrefix(lowerer, decl) + nameText, - ), + qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText), typeParams, instances: new Map(), }); @@ -656,7 +651,7 @@ function internGenericInstance( const typeArgsText = `<${rendered.length > 80 ? rendered.slice(0, 77) + "..." : rendered}>`; inst = { path, - name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""), + name: `${info.qualifiedName}%${info.instances.size}`, ordinal: info.instances.size, params, returnType, @@ -2134,7 +2129,7 @@ function internImplicitInstance( const declared = dynamicBroadPromise ? null : implicitDeclaredReturn(lowerer, info); inst = { path, - name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""), + name: `${info.qualifiedName}%${info.instances.size}`, ordinal: info.instances.size, params: shapes, // The promise callers rely on before the body settles it: the @@ -2322,7 +2317,7 @@ export function implicitLocalFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(lowerer, stmt ?? decl, decl) + `${name}%l${positionTag(lowerer, decl)}`, + nsPathPrefix(stmt ?? decl, decl) + `${name}%l${ts.tokenStartOfNode(decl)}`, ), typeParams: [], instances: new Map(), @@ -2418,7 +2413,7 @@ export function objLitGenericFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(fnNode), - `%ol${positionTag(lowerer, fnNode)}.${name}`, + `%ol${ts.tokenStartOfNode(fnNode)}.${name}`, ), typeParams, instances: new Map(), @@ -2564,7 +2559,7 @@ export function bindingGenericFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(lowerer, stmt ?? decl, decl) + + nsPathPrefix(stmt ?? decl, decl) + (topLevel ? name : `${name}%l${ts.tokenStartOfNode(decl)}`), ), typeParams, diff --git a/packages/compiler/src/frontend/lowering/intern-tables.ts b/packages/compiler/src/frontend/lowering/intern-tables.ts index e3f2b08890..fc69237db1 100644 --- a/packages/compiler/src/frontend/lowering/intern-tables.ts +++ b/packages/compiler/src/frontend/lowering/intern-tables.ts @@ -23,7 +23,6 @@ * The entry handles that itable.find and itable.insert answer stay inside * these lowerings: f64 locals that only reach toBool and itable.value. * Any other use of the class or its members as values is refused. */ -import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { BOOL, F64, STRING, funcOf, typeKey } from "../../ir/ir.js"; import type { IrExpr, IrFunction, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; @@ -448,11 +447,7 @@ function helperName(lowerer: Lowerer, member: InternTableMember, signature: stri } function registerHelper(lowerer: Lowerer, member: InternTableMember, signature: string): string { - const name = keyedHelperName( - lowerer, - `%${INTERN_TABLE_CLASS}.${member}`, - `${member}|${signature}`, - ); + const name = `%${INTERN_TABLE_CLASS}.${member}.${lowerer.internTableHelpers.size}`; lowerer.internTableHelpers.set(`${member}|${signature}`, name); return name; } diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index e50652182e..898c46f205 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { BOOL, @@ -87,7 +86,7 @@ function dynIteratorAdapter( const key = `iterAdapt:${typeKey(expected)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%iter.adapt", key); + name = `%iter.adapt.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const locals: IrLocal[] = []; const local = (id: string, type: IrType, mutable = false): IrExpr => { @@ -314,7 +313,7 @@ export function lowerCollectionIteratorValue( loc, }; if (!typeEquals(yielded.type, type.yieldT)) return null; - name = keyedHelperName(lowerer, "%coll.iter", key); + name = `%coll.iter.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -412,7 +411,7 @@ export function lowerBytesIteratorValue( const key = `bytesIter:${method}:${typeKey(bytesT)}:${typeKey(type)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%bytes.iter", key); + name = `%bytes.iter.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const list = varRef("list.0", bytesT, loc); const index = varRef("index.0", F64, loc); @@ -518,7 +517,7 @@ export function lowerStringIteratorValue( const key = `strIter:${typeKey(type)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%str.iter", key); + name = `%str.iter.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const text = varRef("text.0", STRING, loc); const index = varRef("index.0", F64, loc); @@ -678,7 +677,7 @@ function generatorRetypeAdapter( type: resumeType(expected.nextT), loc, }; - name = keyedHelperName(lowerer, "%gen.retype", key); + name = `%gen.retype.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -795,7 +794,7 @@ export function generatorDrain( const outRef = varRef("out.0", arrT, loc); const value = lowerer.coerceToExpected(genValue(genRef, loc), elem); if (!typeEquals(value.type, elem)) return null; - name = keyedHelperName(lowerer, "%gen.drain", key); + name = `%gen.drain.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -857,7 +856,7 @@ export function generatorDrain( if (!raw) return null; const value = lowerer.coerceToExpected(raw, elem); if (!typeEquals(value.type, elem)) return null; - name = keyedHelperName(lowerer, "%gen.drain", key); + name = `%gen.drain.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -979,7 +978,7 @@ export function lowerArrayIteratorValue( }; } if (!yielded || !typeEquals(yielded.type, type.yieldT)) return null; - name = keyedHelperName(lowerer, "%arr.iter", key); + name = `%arr.iter.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts index e27dd41837..b9bbf62168 100644 --- a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts +++ b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts @@ -21,7 +21,6 @@ * files only, and an empty answer leaves every mapping unchanged, so * TypeScript programs and JavaScript that never uses these patterns * compile exactly as before. */ -import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { isJsSourceFile, locOf } from "../program.js"; import { BOOL, DYN, type IrExpr, type IrStmt } from "../../ir/ir.js"; @@ -216,10 +215,10 @@ export function lowerValueReturningConstructorNew( "spread arguments when constructing a class whose constructor returns a value", ); } - const key = `ctorValue:${lowerer.positions.of(decl)}`; + const key = `ctorValue:${ts.getSourceFileOfNode(decl).fileName}:${ts.tokenStartOfNode(decl)}`; let name = lowerer.valueHelpers.get(key); if (name === undefined) { - name = keyedHelperName(lowerer, "%ctorValue", key); + name = `%ctorValue.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const savedClass = lowerer.currentClass; lowerer.currentClass = null; diff --git a/packages/compiler/src/frontend/lowering/json-require.ts b/packages/compiler/src/frontend/lowering/json-require.ts index d6c2da571d..9277452537 100644 --- a/packages/compiler/src/frontend/lowering/json-require.ts +++ b/packages/compiler/src/frontend/lowering/json-require.ts @@ -10,7 +10,6 @@ * JSON module is ordinary storage holding that object — destructuring * copies its properties at the require, exactly like Node. ES-module JSON * imports keep their own record (Node's ESM loader caches separately). */ -import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { PoisonError } from "./lowerer.js"; @@ -85,7 +84,7 @@ export function collectJsonRequires(lowerer: Lowerer, parts: FileParts[]): void ); throw new PoisonError(); } - const id = keyedHelperName(lowerer, "%g.jsonRequire", jsonSf.fileName); + const id = `%g.jsonRequire.${lowerer.jsonRequireModules.size}`; const global: IrGlobal = { id, name: "%json", type: mapped, mutable: true }; const loadedId = `${id}%loaded`; lowerer.globalsList.push(global, { diff --git a/packages/compiler/src/frontend/lowering/lifetime-regions.ts b/packages/compiler/src/frontend/lowering/lifetime-regions.ts index b684c0395b..b212ae473f 100644 --- a/packages/compiler/src/frontend/lowering/lifetime-regions.ts +++ b/packages/compiler/src/frontend/lowering/lifetime-regions.ts @@ -14,7 +14,6 @@ * Any other use of the class or its methods as values is refused. The * module is marked lifetimeRegions, which moves every ProgramLifetime * class into counted regions. */ -import { keyedHelperName } from "./stable-names.js"; import { F64, VOID, funcOf, typeKey } from "../../ir/ir.js"; import type { IrExpr, IrFunction, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js"; @@ -153,7 +152,7 @@ function runHelper(lowerer: Lowerer, fnType: IrType & { kind: "func" }, loc: Src const key = typeKey(fnType); const known = lowerer.regionRunHelpers.get(key); if (known !== undefined) return known; - const name = keyedHelperName(lowerer, `%${LIFETIME_REGION_CLASS}.run`, key); + const name = `%${LIFETIME_REGION_CLASS}.run.${lowerer.regionRunHelpers.size}`; lowerer.regionRunHelpers.set(key, name); const regionT = lifetimeRegionType(lowerer); const ret = fnType.ret; diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index bc51a93826..460f4d53a7 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { InternalCompilerError } from "../../errors.js"; /* The node:assert lowering (a spoke module like lower-dgram.ts): the * callable module form (`assert(x)` through a default import or a CJS @@ -668,7 +667,7 @@ function partialEqHelper(lowerer: Lowerer, ta: IrType, tb: IrType, loc: SrcLoc): const key = `assert.pdeq:${typeKey(ta)}|${typeKey(tb)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%assert.pdeq", key); + const name = `%assert.pdeq.${lowerer.assertHelpers.size}`; lowerer.assertHelpers.set(key, name); const a = (): IrExpr => varRef("a.0", ta, loc); const b = (): IrExpr => varRef("b.0", tb, loc); @@ -1281,7 +1280,7 @@ function deepCheckCall( const key = `assert.dcheck:${variant}:${negated}:${typeKey(a.type)}:${typeKey(b.type)}`; let name = lowerer.assertHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%assert.dcheck", key); + name = `%assert.dcheck.${lowerer.assertHelpers.size}`; lowerer.assertHelpers.set(key, name); const pa = varRef("a.0", a.type, loc); const pb = varRef("b.0", b.type, loc); @@ -1845,7 +1844,7 @@ function assertThrowsHelper( const key = `assert.${mode}:${recvIsPromise ? "p" : "f"}:${typeKey(recvType)}:${sig}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%assert.${mode}`, key); + const name = `%assert.${mode}.${lowerer.assertHelpers.size}`; lowerer.assertHelpers.set(key, name); const caughtRef = (): IrExpr => varRef("e.0", CAUGHT, loc); @@ -2294,7 +2293,7 @@ function ifErrorHelper(lowerer: Lowerer, t: IrType & { kind: "union" }, loc: Src const key = `assert.iferr:${typeKey(t)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%assert.iferr", key); + const name = `%assert.iferr.${lowerer.assertHelpers.size}`; lowerer.assertHelpers.set(key, name); const def = lowerer.unions.get(t.unionId); if (!def) throw new InternalCompilerError(`assert.ifError over unknown union ${t.unionId}`); @@ -2465,7 +2464,7 @@ function deepEqHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc, loose = false): const key = `assert.${prefix}:${typeKey(t)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%assert.${prefix}`, key); + const name = `%assert.${prefix}.${lowerer.assertHelpers.size}`; lowerer.assertHelpers.set(key, name); const a = (): IrExpr => varRef("a.0", t, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 63b8781f2b..7102f37ab1 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; import { lowerDynStreamMemberCall } from "./dyn-streams.js"; @@ -2095,7 +2094,7 @@ function lowerOptionalNumberDefault( const key = `number.optionalDefault:${widened.unionId}:${dflt}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%number.optionalDefault", key); + helper = `%number.optionalDefault.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", widened, loc); lowerer.liftedFns.push({ @@ -2204,7 +2203,7 @@ function primitiveConversionHandler( const key = `then.${name}:${typeKey(inner)}`; let fnName = lowerer.arrHofHelpers.get(key); if (fnName === undefined) { - fnName = keyedHelperName(lowerer, `%then.${name}`, key); + fnName = `%then.${name}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, fnName); const value = varRef("v.0", inner, loc); let body: IrExpr; @@ -2373,7 +2372,7 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc) const key = `bigint.scalar:${unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%bigint.scalar", key); + helper = `%bigint.scalar.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); const body: IrStmt[] = []; @@ -3235,7 +3234,7 @@ function lowerScalarUnionNumber( const key = `number.scalar:${arg.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%number.scalar", key); + helper = `%number.scalar.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); const nan: IrExpr = { @@ -3280,7 +3279,7 @@ function lowerOptionalStringNumber( const key = `${label}.optionalString:${arg.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%${label}.optionalString`, key); + helper = `%${label}.optionalString.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); lowerer.liftedFns.push({ @@ -4707,7 +4706,7 @@ function lowerDefaultToStringCall( const key = `objToStr:${typeKey(recv.type)}${missing ? `:${missing}` : ""}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%obj.tostr", key); + helper = `%obj.tostr.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const o = varRef("o.0", recv.type, loc); lowerer.liftedFns.push({ @@ -5394,7 +5393,7 @@ function loweredTemplateStrings( const site = lowerer.globalsList.length; const globalId = `%g.templateStrings.${site}`; const readyId = `%g.templateStringsReady.${site}`; - getter = keyedHelperName(lowerer, "%templateStrings.get", key); + getter = `%templateStrings.get.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, getter); lowerer.globalsList.push( { id: globalId, name: "%templateStrings", type, mutable: true }, @@ -5575,7 +5574,7 @@ export function lowerPromiseMethodCall( // IS the spec's identity/thrower/no-op substitution for all three // members. An argument expression, when present, is undefined/null // by construction — nothing to evaluate. - const fnName = lowerer.closureName(loc, `_${member}pass`); + const fnName = `%fn${lowerer.lambdaCounter++}_${member}pass`; const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT], ret: promT }; const fnCtx = newFnCtx(true, null, funcType, inner); fnCtx.isAsync = true; @@ -5672,7 +5671,7 @@ export function lowerPromiseMethodCall( // Evaluate both callback expressions before the wrapper runs, in source order. const fulfilled = fulfilledAbsent ? null : boxHandler(fulfilledNode, true); const rejected = rejectedAbsent ? null : boxHandler(rejectedNode, false); - const fnName = lowerer.closureName(loc, "_then2"); + const fnName = `%fn${lowerer.lambdaCounter++}_then2`; const paramTypes: IrType[] = [ promT, ...(fulfilled ? [fulfilled.value.type] : []), @@ -5921,7 +5920,7 @@ export function lowerPromiseMethodCall( ); }; const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: DYN }; - const fnName = lowerer.closureName(loc, "_then"); + const fnName = `%fn${lowerer.lambdaCounter++}_then`; const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, DYN], @@ -6016,7 +6015,7 @@ export function lowerPromiseMethodCall( ); } const R = resultT.inner; - const fnName = lowerer.closureName(loc, "_then"); + const fnName = `%fn${lowerer.lambdaCounter++}_then`; const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, cb.type], @@ -6119,7 +6118,7 @@ export function lowerPromiseMethodCall( "finally callbacks with parameters or a return value (use () => { ... })", ); } - const fnName = lowerer.closureName(loc, "_finally"); + const fnName = `%fn${lowerer.lambdaCounter++}_finally`; const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, cb.type], @@ -6222,7 +6221,7 @@ export function lowerPromiseMethodCall( } if (cb.type.kind === "dyn") { const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: DYN }; - const fnName = lowerer.closureName(loc, "_catchdyn"); + const fnName = `%fn${lowerer.lambdaCounter++}_catchdyn`; const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, DYN], @@ -6352,7 +6351,7 @@ export function lowerPromiseMethodCall( ); } const R = resultT.inner; - const fnName = lowerer.closureName(loc, "_catch"); + const fnName = `%fn${lowerer.lambdaCounter++}_catch`; const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT], ret: resultT }; const fnCtx = newFnCtx(true, null, funcType, R); fnCtx.isAsync = true; @@ -6589,7 +6588,7 @@ function filterNarrowHelper( const key = `filterNarrow:${test}:${typeKey(elem)}:${typeKey(outElem)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%arr.filterNarrow", key); + const name = `%arr.filterNarrow.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); @@ -6835,7 +6834,7 @@ export function recordKeysArrayCall( const key = `obj.keys:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%obj.keys", key); + helper = `%obj.keys.${lowerer.arrHofHelpers.size}`; const ref: IrExpr = { kind: "varRef", localId: "r.0", type: argIr, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: resultT, loc }; lowerer.arrHofHelpers.set(key, helper); @@ -6905,7 +6904,7 @@ function recordHasOwnHelper(lowerer: Lowerer, shapeId: string, loc: SrcLoc): str const key = `obj.hasOwn:${shapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = keyedHelperName(lowerer, "%obj.hasOwn", key); + const helper = `%obj.hasOwn.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const shape = lowerer.shapes.get(shapeId)!; const recT: IrType = { kind: "record", shapeId }; @@ -6990,7 +6989,7 @@ function classAssignHelper( const key = `obj.assignClass:${className}:${srcShapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = keyedHelperName(lowerer, "%obj.assignClass", key); + const helper = `%obj.assignClass.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const sShape = lowerer.shapes.get(srcShapeId)!; const oT: IrType = { kind: "object", className }; @@ -7058,7 +7057,7 @@ function recordAssignHelper( const key = `obj.assign:${targetShapeId}:${srcShapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = keyedHelperName(lowerer, "%obj.assign", key); + const helper = `%obj.assign.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const sShape = lowerer.shapes.get(srcShapeId)!; const tShape = lowerer.shapes.get(targetShapeId)!; @@ -8515,7 +8514,7 @@ function lowerObjectStaticCall( const key = `obj.${member}:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%obj.${member}`, key); + helper = `%obj.${member}.${lowerer.arrHofHelpers.size}`; const recT = argIr; const ref: IrExpr = { kind: "varRef", localId: "r.0", type: recT, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: resultT, loc }; @@ -9151,7 +9150,7 @@ function lowerInlineArrowBind( const length = firstDefault < 0 ? arrow.parameters.nodes.length : firstDefault; const key = `arrow.bind:${typeKey(type)}:${length}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%arrow.bind", key); + const name = existing ?? `%arrow.bind.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); @@ -10080,7 +10079,7 @@ function lowerUnionObjectDynFieldCall( const receiverRef = varRef(receiverLocal.id, receiverT, loc); let helper = lowerer.unionCallHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%union.call", key); + helper = `%union.call.${lowerer.unionCallHelpers.size}`; lowerer.unionCallHelpers.set(key, helper); const receiverRef = varRef("this.0", receiverT, loc); const branch = (plan: (typeof plans)[number]): IrStmt[] => { @@ -10173,7 +10172,7 @@ function unionObjectMethodHelper( const key = `${receiverT.unionId}:${method}:${shapes.map((shape) => `${shape.mode}:${typeKey(shape.type)}`).join(",")}:${typeKey(resultT)}`; const existing = lowerer.unionCallHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%union.call", key); + const name = `%union.call.${lowerer.unionCallHelpers.size}`; lowerer.unionCallHelpers.set(key, name); // A probe that catches a refusal while the body lowers must not leave // the name registered for a helper that was never emitted. @@ -10367,7 +10366,7 @@ export function lowerStaticNumberGlobal( const key = `parseInt.optional:${unionT.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%parseInt.optional", key); + helper = `%parseInt.optional.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const value: IrExpr = { kind: "varRef", localId: "value.0", type: unionT, loc }; const radixRef: IrExpr = { kind: "varRef", localId: "radix.0", type: F64, loc }; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index fcea03790e..790efe988d 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; import { adaptPromiseExecutor } from "./promise-executor-adapter.js"; import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; @@ -4664,7 +4663,7 @@ export function genericClassInstanceType( ); } const ordinal = gci.instances.size; - const name = keyedHelperName(lowerer, `${gci.family.def.name}%`, key, ""); + const name = `${gci.family.def.name}%${ordinal}`; const entry: { name: string; info: ClassInfo | null; poisoned?: boolean } = { name, info: null }; gci.instances.set(key, entry); const bindings = new Map(); @@ -7911,7 +7910,7 @@ export function classIteratorDrainCall( const key = `${cit.className}:${typeKey(outElem)}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%iter.drain", key); + name = `%iter.drain.${lowerer.iterDrainHelpers.size}`; lowerer.iterDrainHelpers.set(key, name); const recvT: IrType = { kind: "object", className: cit.className }; const recvRef: IrExpr = { kind: "varRef", localId: "r.0", type: recvT, loc }; @@ -8025,7 +8024,7 @@ export function classIteratorRestDrainCall( const key = `it:${cit.iterT.className}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%iter.drain", key); + name = `%iter.drain.${lowerer.iterDrainHelpers.size}`; lowerer.iterDrainHelpers.set(key, name); const itRef: IrExpr = { kind: "varRef", localId: "it.0", type: cit.iterT, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: outT, loc }; @@ -10533,7 +10532,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const key = `x509.record:${isBytes ? "bytes" : "str"}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%x509.record", key); + helper = `%x509.record.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const dataT = data.type; const dRef: IrExpr = { kind: "varRef", localId: "d.0", type: dataT, loc }; diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index aa90fc7c74..5c99ebc821 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, boolLit } from "../../ir/build.js"; /* The dgram/dns-surface lowering (node:dgram + node:dns — a spoke module * like lower-server.ts): module-function calls (dgram createSocket, dns @@ -358,7 +357,7 @@ function checkedDynPromise( const key = `dns.promise:${typeKey(declared)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%dns.promise", key); + helper = `%dns.promise.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); const promiseT: IrType = { kind: "promise", inner: DYN }; lowerer.liftedFns.push({ @@ -892,7 +891,7 @@ export function lowerPromisifiedDnsCall( const key = `dnsPromise:${JSON.stringify(value)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%dnsPromise", key); + name = `%dnsPromise.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index f105af786d..f472672b20 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -42,7 +42,6 @@ * emit-overrides block at the bottom of this file (per-event * monomorphization of the override body, dispatch on the dynamic class, * super.emit as the prototype chain). */ -import { keyedHelperName } from "./stable-names.js"; import { checkKeypressListener } from "./builtins/readline.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; @@ -632,7 +631,7 @@ function lowerFlexListenerCall( const key = `emitter.${registering ? "onFlex" : "offDyn"}:${typeKey(recvT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, `%emitter.${registering ? "onFlex" : "offFlex"}`, key); + helper = `%emitter.${registering ? "onFlex" : "offFlex"}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -851,7 +850,7 @@ function lowerDynListenerCall( const existing = lowerer.arrHofHelpers.get(key); let helper = existing; if (!helper) { - helper = keyedHelperName(lowerer, `%emitter.${registering ? "onDyn" : "offDyn"}`, key); + helper = `%emitter.${registering ? "onDyn" : "offDyn"}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const check: IrStmt = { kind: "exprStmt", @@ -1001,7 +1000,7 @@ export function lowerDynamicEmitterEmit( const key = `emitter.dynEmit:${typeKey(recvT)}:${k}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%emitter.dynEmit", key); + helper = `%emitter.dynEmit.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -1093,7 +1092,7 @@ export function lowerDynamicEmitterOn( const key = `emitter.dynOn:${typeKey(recvT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%emitter.dynOn", key); + helper = `%emitter.dynOn.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -2360,7 +2359,7 @@ function emitDispatchExpr( const key = `emitter.emitDispatch:${typeKey(recvT)}:${name}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = keyedHelperName(lowerer, "%emitter.emitDispatch", key); + helper = `%emitter.emitDispatch.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -2425,7 +2424,7 @@ function eventsOnceBoxer( const key = `eventsOnce:${typeKey(fnType)}`; let name = lowerer.valueHelpers.get(key); if (name === undefined) { - name = keyedHelperName(lowerer, "%events.onceArgs", key); + name = `%events.onceArgs.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const params: IrParam[] = tuple.map((type, i) => ({ localId: `%once.a${i}`, diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index ba96bea8ca..e7a1b79a83 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; @@ -2275,11 +2274,7 @@ export function cjsAccessorSnapshotOf( if (!lexerVisibleAccessors(ts.getSourceFileOfNode(getter)).includes(getter)) return null; const call = cjsAccessorCall(lowerer, getter, locOf(getter)); if (call === null) return null; - const at = locOf(getter); - const snapshot = { - id: keyedHelperName(lowerer, "%g.cjsSnapshot", `${at.file}:${at.start}:${at.end}`), - type: call.type, - }; + const snapshot = { id: `%g.cjsSnapshot.${lowerer.cjsAccessorSnapshots.size}`, type: call.type }; lowerer.globalsList.push({ id: snapshot.id, name: "%cjsSnapshot", @@ -2304,7 +2299,7 @@ export function cjsAccessorSnapshotStores( if (getters.length === 0) return []; let readyId = lowerer.cjsSnapshotReady.get(dep); if (readyId === undefined) { - readyId = keyedHelperName(lowerer, "%g.cjsSnapshotReady", dep.fileName); + readyId = `%g.cjsSnapshotReady.${lowerer.cjsSnapshotReady.size}`; lowerer.globalsList.push({ id: readyId, name: "%cjsSnapshotReady", type: BOOL, mutable: true }); lowerer.cjsSnapshotReady.set(dep, readyId); } diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 653c0d837e..cd8dc31d1d 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -270,7 +270,7 @@ export function lowerLambda( const nameIdent = !ts.isArrowFunction(node) && node.name && ts.isIdentifier(node.name) ? node.name : null; const baseName = nameIdent ? nameIdent.text : ""; - const fnName = lowerer.closureName(locOf(node), baseName ? `_${baseName}` : ""); + const fnName = `%fn${lowerer.lambdaCounter++}${baseName ? `_${baseName}` : ""}`; // Named function expressions/declarations can self-reference by name; an // object-literal method's name is a PROPERTY, not a binding — no self. const selfSymbol = diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 1979b43aa8..4be5d0b7ef 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, boolLit, @@ -269,7 +268,7 @@ function errorInspectHelper( const key = `insp.userError:${typeKey(t)}`; const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%util.insp", key); + const name = `%util.insp.${lowerer.inspectHelpers.size}`; lowerer.inspectHelpers.set(key, name); const v = (): IrExpr => varRef("v.0", t, loc); const r = (): IrExpr => varRef("r.0", F64, loc); @@ -1146,7 +1145,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { const key = `insp:${typeKey(t)}`; const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, "%util.insp", key); + const name = `%util.insp.${lowerer.inspectHelpers.size}`; lowerer.inspectHelpers.set(key, name); const v = (): IrExpr => varRef("v.0", t, loc); @@ -2075,7 +2074,7 @@ function formatUnionHelper( const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; const loc = helperLoc(site); - const name = keyedHelperName(lowerer, "%util.fmtv", key); + const name = `%util.fmtv.${lowerer.inspectHelpers.size}`; lowerer.inspectHelpers.set(key, name); const def = lowerer.unions.get(t.unionId); if (!def) throw new InternalCompilerError(`format value over unknown union ${t.unionId}`); diff --git a/packages/compiler/src/frontend/lowering/lower-mixins.ts b/packages/compiler/src/frontend/lowering/lower-mixins.ts index 9de027563f..766059dfb9 100644 --- a/packages/compiler/src/frontend/lowering/lower-mixins.ts +++ b/packages/compiler/src/frontend/lowering/lower-mixins.ts @@ -34,7 +34,6 @@ * statics-bearing mixin classes outside a plain top-level statement * (their declaration-time code must run exactly where the call * evaluates). */ -import { positionTag } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { PoisonError } from "./lowerer.js"; @@ -453,7 +452,7 @@ function instantiateMixinCall( const base = mixinBaseClassOf(lowerer, call.arguments.nodes[0]!); const instName = lowerer.qualify( ts.getSourceFileOfNode(call), - `%mx${positionTag(lowerer, call)}.${shape.classNode.name?.text ?? ""}`, + `%mx${ts.tokenStartOfNode(call)}.${shape.classNode.name?.text ?? ""}`, ); const ordinal = lowerer.mixinOrdinals.get(shape.classNode) ?? 0; lowerer.mixinOrdinals.set(shape.classNode, ordinal + 1); diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts index 8c50dee460..fcc68c787a 100644 --- a/packages/compiler/src/frontend/lowering/lower-mock.ts +++ b/packages/compiler/src/frontend/lowering/lower-mock.ts @@ -17,7 +17,6 @@ * writes the original back on restore(). Each mock registers a closure * that calls its restore() with its tracker (scr_test.c): the module's * `mock`, or the test's t.mock, which is reset when the test ends. */ -import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { BOOL, @@ -137,7 +136,7 @@ function liftOnce( const key = `mock.${role}:${owner}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%mock.${role}`, key); + const name = `%mock.${role}.${lowerer.assertHelpers.size}`; lowerer.assertHelpers.set(key, name); build(name); return name; diff --git a/packages/compiler/src/frontend/lowering/lower-modules.test.ts b/packages/compiler/src/frontend/lowering/lower-modules.test.ts index f1021e4da8..84cc96e7dd 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.test.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.test.ts @@ -34,7 +34,7 @@ test("retains dependencies from function storage, captures, generator channels a const completed = record("completed"); const body = record("body"); const global = record("global", record("nested")); - const wanted = lowerer.shapes.shapes.map((shape) => shape.id).sort(); + const wanted = lowerer.shapes.shapes.map((shape) => shape.id); record("unused"); const unionId = lowerer.unions.intern([body, UNDEFINED_T]); const unusedUnion = lowerer.unions.intern([F64, UNDEFINED_T]); @@ -162,7 +162,7 @@ test("closes nested structural types and index-signature values", () => { sent = record("sent"), indexed = record("indexed"); const containerId = lowerer.shapes.intern([], false, indexed); - const wanted = lowerer.shapes.shapes.map((shape) => shape.id).sort(); + const wanted = lowerer.shapes.shapes.map((shape) => shape.id); record("unused"); const nested: IrType = { kind: "func", diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 0b99e88d3e..d1f276d44b 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -1,5 +1,4 @@ import { THREADS_MODULE } from "../threads-import.js"; -import { positionTag } from "./structural-positions.js"; import { EXPERIMENTAL_MODULE } from "../experimental-import.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { everyStmtList, everyTypeChild } from "../../ir/traverse.js"; @@ -1075,14 +1074,8 @@ export function moduleArtifacts( else delete rest.abstractMethods; return rest; }), - // Ids come from what each shape and union is (keyedHelperName), so - // ordering by id gives every lowering order the same module. - records: lowerer.shapes.shapes - .filter((r) => shapeIds.has(r.id)) - .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), - unions: lowerer.unions.unions - .filter((u) => unionIds.has(u.id)) - .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), + records: lowerer.shapes.shapes.filter((r) => shapeIds.has(r.id)), + unions: lowerer.unions.unions.filter((u) => unionIds.has(u.id)), }; } @@ -1684,7 +1677,7 @@ export function collectGlobals( } if (type.kind === "void") lowerer.badType(stmt.name, lowerer.typeOf(stmt.name)); const g: IrGlobal = { - id: `%g.${tag}${nsPathPrefix(lowerer, stmt)}%alias.${stmt.name.text}%${positionTag(lowerer, stmt)}`, + id: `%g.${tag}${nsPathPrefix(stmt)}%alias.${stmt.name.text}%${ts.tokenStartOfNode(stmt)}`, name: stmt.name.text, type, mutable: false, @@ -1761,7 +1754,7 @@ export function collectGlobals( // (`%g.e.A%B.x`), so `namespace A { export const x }` and a // top-level `const x` never collide; non-exported members add the // declaring block's position (block-local bindings). - const nsPrefix = nsPathPrefix(lowerer, stmt, list.declarations.nodes[0]); + const nsPrefix = nsPathPrefix(stmt, list.declarations.nodes[0]); for (const decl of list.declarations.nodes) { // --provenance-sources: an elided pure-annotated dead const in a // fetched source module registers no global (the statement diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 19123dae3d..dca835d779 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -36,7 +36,6 @@ * of the compiler already takes (a body running before the declaring * statement is not modeled — same as top-level const reads through early * calls). */ -import { positionTag, type PositionTagOwner } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import { packageShimFor } from "../package-shims.js"; import type { Lowerer } from "./lowerer.js"; @@ -165,11 +164,7 @@ function registerTypeOnlyBlocks(lowerer: Lowerer, decl: ts.ModuleDeclaration): v * distinct (resolution is by symbol; names only need uniqueness and * determinism). `modifierDecl` carries the declaration whose modifiers * decide exportedness (the first declarator of a variable statement). */ -export function nsPathPrefix( - owner: PositionTagOwner, - node: ts.Node, - modifierDecl?: ts.Node, -): string { +export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string { const parent = node.parent; if (parent === undefined || !ts.isModuleBlock(parent)) return ""; const flagsHost = modifierDecl ?? node; @@ -184,10 +179,10 @@ export function nsPathPrefix( const nested = p.parent !== undefined && ts.isModuleBlock(p.parent); const nonExported = nested && (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Export) === 0; - segs.unshift(p.name.text + (nonExported ? `%${positionTag(owner, p)}` : "")); + segs.unshift(p.name.text + (nonExported ? `%${ts.tokenStartOfNode(p)}` : "")); } } - return `${segs.join("%")}${exported ? "" : `%${positionTag(owner, parent)}`}.`; + return `${segs.join("%")}${exported ? "" : `%${ts.tokenStartOfNode(parent)}`}.`; } /** The registered namespace-block kind a symbol's declaration sits in diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 94d49d8b37..058d7874aa 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, nodeThrowExpr, @@ -184,7 +183,7 @@ export function voidizedCallback(lowerer: Lowerer, cb: IrExpr, loc: SrcLoc): IrE const toT = funcOf([...fromT.params], VOID); const key = `server.voidcb:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%server.voidcb", key); + const name = existing ?? `%server.voidcb.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; @@ -280,7 +279,7 @@ function receiverReturningCall( ): IrExpr { const key = `server.recvret:${fn}:${opts?.prefix?.fn ?? ""}:${(opts?.mainArgIndices ?? []).join(".")}:${callArgs.map((a) => typeKey(a.type)).join(",")}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%server.recvret", key); + const name = existing ?? `%server.recvret.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); const params = callArgs.map((a, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: a.type })); @@ -591,7 +590,7 @@ function h2HeadersCallbackAdapter( ); const key = `h2.hdrcb:${handleKind}:${withStatus}:${typeKey(fromT)}:${shapeId ?? ""}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%h2.hdrcb", key); + const name = existing ?? `%h2.hdrcb.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; @@ -3584,7 +3583,7 @@ export function lowerCompatReqStreamOptionalCall( type: unionT, loc, }; - const id = lowerer.closureName(loc, "", "chain"); + const id = `chain.${lowerer.chainCounter++}`; const body = lowerH2StreamMethodCall(lowerer, call, call.expression, { kind: "chainRecv", id, @@ -4425,7 +4424,7 @@ function lowerHttpCreateServerForms( const hasMax = opts.maxHeaderSize !== null; const key = `server.opts:${opts.joinDup ? 1 : 0}:${opts.allowMissingHost ? 1 : 0}:${hasBuffer ? 1 : 0}:${hasMax ? 1 : 0}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%server.opts", key); + const name = existing ?? `%server.opts.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); const params = [ @@ -8010,7 +8009,7 @@ function lowerHttpResMethodCall( const all = [...callArgs, cb]; const key = `server.endcb:${fn}:${all.map((a) => typeKey(a.type)).join(",")}`; const existing = lowerer.arrHofHelpers.get(key); - const helper = existing ?? keyedHelperName(lowerer, "%server.endcb", key); + const helper = existing ?? `%server.endcb.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, helper); const params = all.map((a, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: a.type })); diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 0beb24a0ea..2ad3657c7b 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -2278,7 +2278,7 @@ function lowerStaticNamespaceImport( ): IrExpr { const loc = locOf(call); const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: ns }; - const fnName = lowerer.closureName(loc, "_staticImport"); + const fnName = `%fn${lowerer.lambdaCounter++}_staticImport`; const fnType: IrType & { kind: "func" } = { kind: "func", params: [], ret: resultT }; const fnCtx = newFnCtx(true, null, fnType, ns); fnCtx.isAsync = true; diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index a75ec43b72..b9add95a61 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { nodeThrowExpr, boolLit, numLit, strLit } from "../../ir/build.js"; import { hashStreamOf, isHashValueType } from "./builtins/hash-stream.js"; import { InternalCompilerError } from "../../errors.js"; @@ -312,7 +311,7 @@ function lowerStreamCallback( ); } } - const fnName = lowerer.closureName(loc, `_${which}cb`); + const fnName = `%fn${lowerer.lambdaCounter++}_${which}cb`; const fnCtx = newFnCtx(true, null, funcType, funcType.ret); lowerer.fnStack.push(fnCtx); try { @@ -1145,7 +1144,7 @@ function streamMethodWrapper( paramTypes.push(got.type); } const ret = found.sig.ret; - const fnName = lowerer.closureName(loc, `_${which}vt`); + const fnName = `%fn${lowerer.lambdaCounter++}_${which}vt`; const funcType = funcOf([thisType, ...paramTypes], ret); lowerer.fnStack.push(newFnCtx(true, null, funcType, ret)); try { @@ -1691,7 +1690,7 @@ function fromGenHelpers( const key = `streamFromGen:${role}:${typeKey(gen)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, `%stream.fromGen.${role}`, key); + name = `%stream.fromGen.${role}.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -1780,7 +1779,7 @@ function classIteratorObject( const key = `streamFromIter:${cit.iterT.className}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%stream.fromIter", key); + name = `%stream.fromIter.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const nextName = `${name}.next`; const nextT = funcOf([], DYN); @@ -1941,7 +1940,7 @@ function classAsyncIteratorObject( const key = `streamFromAsyncIter:${cit.className}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%stream.fromAsyncIter", key); + name = `%stream.fromAsyncIter.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const it: IrLocal = { id: "it.0", name: "it", type: cit.iterT, mutable: false, boxed: true }; const src: IrLocal = { @@ -2895,7 +2894,7 @@ function pipelineBoxHelper( const key = `pipelineBox:${typeKey(promiseT)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = keyedHelperName(lowerer, "%stream.pipelineBox", key); + name = `%stream.pipelineBox.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -2922,7 +2921,7 @@ function pipelineAbortHelper(lowerer: Lowerer, ac: IrExpr, loc: SrcLoc): IrExpr let name = lowerer.valueHelpers.get(key); const abortT = funcOf([], VOID); if (!name) { - name = keyedHelperName(lowerer, "%stream.pipelineAbort", key); + name = `%stream.pipelineAbort.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const acLocal: IrLocal = { id: "ac.0", name: "ac", type: DYN, mutable: false, boxed: true }; const acRef = varRef(acLocal.id, DYN, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index 03c9c5c1f2..7d4bba34b6 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; import { isJsSourceFile } from "../program.js"; @@ -56,7 +55,7 @@ export function adaptZeroArgTimerCallback( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.dropret:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%timer.dropret", key); + const name = existing ?? `%timer.dropret.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( @@ -118,7 +117,7 @@ export function adaptZeroArgTimerCallback( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.droparg:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%timer.droparg", key); + const name = existing ?? `%timer.droparg.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( @@ -204,7 +203,7 @@ function makeTimerArgsThunk( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.argsthunk:${n}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? keyedHelperName(lowerer, "%timer.argsthunk", key); + const name = existing ?? `%timer.argsthunk.${lowerer.arrHofHelpers.size}`; if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; diff --git a/packages/compiler/src/frontend/lowering/lowerer.test.ts b/packages/compiler/src/frontend/lowering/lowerer.test.ts index 7fbb9661fc..bacdc7941c 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.test.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.test.ts @@ -10,7 +10,6 @@ import { } from "../../ir/ir.js"; import type { Node, Program, SourceFile, Symbol, Type } from "../ts7/adapter.js"; import { Lowerer } from "./lowerer.js"; -import { UnionRegistry } from "../type-mapper.js"; import { classMethodValue } from "./class-method-values.js"; const loc = { file: "island.ts", start: 0, end: 1 }; @@ -33,7 +32,7 @@ test("reuses complete copy routes without repeating discriminator planning", () expect(plan).not.toHaveBeenCalled(); expect(lowerer.liftedFns.filter((fn) => fn.name === helper)).toHaveLength(1); const recursive = {} as Type; - lowerer.shapes.recursiveRef(recursive, () => "Recursive"); + lowerer.shapes.recursiveRef(recursive); lowerer.shapes.finalizeRecursive(recursive, [{ name: "kind", type: STRING }]); expect(lowerer.unionRetagHelper(from, to, loc)).toBe(helper); expect(plan).toHaveBeenCalled(); @@ -55,7 +54,7 @@ test("replans after a recursive union gains its final arms", () => { const lowerer = context(); const recursive = {} as Type; const from = lowerer.unions.intern([UNDEFINED_T, NULL_T]); - const to = lowerer.unions.recursiveRef(recursive, () => "Recursive"); + const to = lowerer.unions.recursiveRef(recursive); const trapped = lowerer.unionRetagHelper(from, to, loc); expect(trapped).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [UNDEFINED_T]); @@ -76,7 +75,7 @@ test("rechecks width conversions after recursive record definitions settle", () const lowerer = context(); const recursive = {} as Type; const source = record(lowerer.shapes.intern([{ name: "value", type: F64 }])); - const target = record(lowerer.shapes.recursiveRef(recursive, () => "Recursive")); + const target = record(lowerer.shapes.recursiveRef(recursive)); const from = lowerer.unions.intern([source, UNDEFINED_T]); const to = lowerer.unions.intern([target, NULL_T]); expect(lowerer.unionRetagHelper(from, to, loc)).not.toBeNull(); @@ -130,7 +129,7 @@ test("literal ownership is reused across values and rechecked when recursive rec expect(inspect).not.toHaveBeenCalled(); const recursive = {} as Type; - const pendingShape = record(lowerer.shapes.recursiveRef(recursive, () => "Recursive")); + const pendingShape = record(lowerer.shapes.recursiveRef(recursive)); const pending = lowerer.unions.intern([pendingShape], { field: "kind", cases: [{ tag: 0, values: ["a"] }], @@ -144,7 +143,7 @@ test("literal ownership is reused across values and rechecked when recursive rec test("literal ownership sees a recursive union's finalized discriminator", () => { const lowerer = context(); const recursive = {} as Type; - const union = lowerer.unions.recursiveRef(recursive, () => "Recursive"); + const union = lowerer.unions.recursiveRef(recursive); const shape = record(lowerer.shapes.intern([{ name: "kind", type: STRING }])); expect(lowerer.literalUnionArm(union, ["ready"])).toBeNull(); lowerer.unions.finalizeRecursive(recursive, [shape], { @@ -183,10 +182,7 @@ test("removing undefined reuses the discriminator transformation without hiding ); const recursive = {} as Type; - const pending: IrType = { - kind: "union", - unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), - }; + const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; expect(lowerer.stripUndefinedArm(pending)).toEqual(pending); lowerer.unions.finalizeRecursive(recursive, [STRING, UNDEFINED_T]); expect(lowerer.stripUndefinedArm(pending)).toEqual(STRING); @@ -219,10 +215,7 @@ test("adding undefined reuses semantic union transformations and sees recursive expect(lowerer.unions.get(added.unionId)?.discriminant).toEqual(discriminant); const recursive = {} as Type; - const pending: IrType = { - kind: "union", - unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), - }; + const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; const before = lowerer.withUndefinedArmOf(pending); lowerer.unions.finalizeRecursive(recursive, [a, b], discriminant); expect(lowerer.withUndefinedArmOf(pending)).toEqual(added); @@ -234,18 +227,13 @@ test("adding undefined reuses semantic union transformations and sees recursive test("optional union widening keeps refused and missing contracts current", () => { const lowerer = context(); const recursive = {} as Type; - const pending: IrType = { - kind: "union", - unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), - }; + const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; expect(lowerer.withUndefinedArmOf(pending)).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [{ kind: "date" }, STRING]); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); - // Union ids come from their arms, so a fresh registry names the union - // this one has not interned yet. - const missing: IrType = { kind: "union", unionId: new UnionRegistry().intern([F64, STRING]) }; + const missing: IrType = { kind: "union", unionId: `u${lowerer.unions.unions.length}` }; expect(lowerer.withUndefinedArmOf(missing)).toBeNull(); expect(lowerer.unions.intern([F64, STRING])).toBe(missing.unionId); expect(lowerer.withUndefinedArmOf(missing)?.kind).toBe("union"); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3f8a0a66e5..ee8ba7ecba 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,6 +1,4 @@ import { relieveHeapPressure } from "../../heap-pressure.js"; -import { keyedHelperName } from "./stable-names.js"; -import { positionTag, StructuralPositions } from "./structural-positions.js"; import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js"; import { needsGeneralNodeStreams, @@ -1006,11 +1004,6 @@ function lowerShuffle(): ((wave: string[]) => void) | null { * the bodies reachable emit did NOT mark (plus deferred collection * diagnostics nothing flushed), reported separately: whole-program analysis without * letting unreached code fail builds. */ -/** The closure root of an emit-override specialization body. */ -function emitSpecRoot(spec: EmitSpecRequest): string { - return `%emit.${spec.info.def.name}.${spec.event}.${spec.ordinal}`; -} - export function lowerToIr( program: ts.Program, entry: ts.SourceFile, @@ -2808,8 +2801,6 @@ export class Lowerer { /** Generated value helpers, keyed by operation and type. Callers own * their key prefixes; this includes more than structural conversions. */ readonly valueHelpers = new Map(); - /** Each name keyedHelperName made, with a second hash of its key. */ - readonly keyedNameKeys = new Map(); /** structuralNarrowing's assignable classes, keyed by static and * narrowed class name. */ readonly structuralClassPeers = new Map(); @@ -2946,6 +2937,7 @@ export class Lowerer { ctx: FnCtx; } >(); + chainCounter = 0; /** Keyed by program-wide qualified class name (what IR object types carry). */ readonly classes = new Map(); readonly classBySymbol = new Map(); @@ -3175,47 +3167,7 @@ export class Lowerer { * dispatch revision needs no weak-key identity or process-global state. */ errorMethodDispatchRevision: string | null = null; errorPropertyDispatchRevision: string | null = null; - /** Structural source positions (structural-positions.ts) for names that - * stay the same when unrelated code moves. */ - readonly positions = new StructuralPositions(); - - /** The outermost node whose token span is exactly `loc`, or null. */ - nodeAtLoc(loc: SrcLoc): ts.Node | null { - const sf = this.program.getSourceFile(loc.file); - return sf === undefined ? null : (ts.nodeAtSpan(sf, loc.start, loc.end) ?? null); - } - - /** The lowering unit whose name scopes the closures it lifts. */ - private closureRoot = "%pre"; - private readonly closureNameCounts = new Map(); - - /** Runs `fn` with the closures it lifts named under `root`. */ - withClosureRoot(root: string, fn: () => T): T { - const previous = this.closureRoot; - this.closureRoot = root; - try { - return fn(); - } finally { - this.closureRoot = previous; - } - } - - /** The name of a closure (or wrapper) lifted for the source at `loc`: - * `.` of the current lowering unit, the location and - * `suffix`, then `suffix`. A unit that lifts several functions for the same - * location and suffix numbers them in the order it lifts them, so a - * closure's name depends only on its unit, not on which units lowered - * before it. */ - closureName(loc: SrcLoc, suffix = "", prefix = "%fn"): string { - const node = this.nodeAtLoc(loc); - const where = node === null ? `${loc.file}:${loc.start}:${loc.end}` : this.positions.of(node); - const key = `${prefix}|${this.closureRoot}|${where}|${suffix}`; - const first = keyedHelperName(this, prefix, key); - const seen = this.closureNameCounts.get(first) ?? 0; - this.closureNameCounts.set(first, seen + 1); - return (seen === 0 ? first : keyedHelperName(this, prefix, `${key}|${seen}`)) + suffix; - } - + lambdaCounter = 0; readonly preparedClassFactories = new Map(); readonly preparingClassFactories = new Set(); @@ -3568,7 +3520,6 @@ export class Lowerer { this.checker = program.getTypeChecker(); this.typeCtx = { checker: this.checker, - positionOf: (node) => this.positions.of(node), shapes: this.shapes, unions: this.unions, classNamer: this.classNamer, @@ -3857,11 +3808,11 @@ export class Lowerer { })() ? this.qualify( ts.getSourceFileOfNode(decl), - `%cx${positionTag(this, decl)}.${decl.name?.text ?? ""}`, + `%cx${ts.tokenStartOfNode(decl)}.${decl.name?.text ?? ""}`, ) : this.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(this, decl) + (decl.name ? decl.name.text : "%anon"), + nsPathPrefix(decl) + (decl.name ? decl.name.text : "%anon"), ); for (let index = this.localClassInstantiations.length - 1; index >= 0; index--) { const entry = this.localClassInstantiations[index]!; @@ -7420,10 +7371,7 @@ export class Lowerer { } for (const decl of fp.classDecls) { const info = this.classes.get(this.classNamer(decl)); - if (info) - functions.push( - ...this.withClosureRoot(info.def.name, () => this.lowerClassMembers(info)), - ); + if (info) functions.push(...this.lowerClassMembers(info)); else if (this.countsSkips()) this.countSkippedBody(decl); } } @@ -7436,10 +7384,7 @@ export class Lowerer { // are reachable by definition, already counted by reachable emit. if (!this.remainder) { for (const fp of parts) { - const initName = this.initNameOf.get(fp.sf)!; - functions.push( - this.withClosureRoot(initName, () => this.lowerFileInit(fp.sf, fp.topStmts, initName)), - ); + functions.push(this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!)); } functions.push(this.buildMain()); } @@ -7481,9 +7426,7 @@ export class Lowerer { // default): the diagnostic is recorded — the instance skips // like a signature-blocked function (lowerFunction's rule). try { - functions.push( - this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), - ); + functions.push(this.lowerGenericInstance(info, inst)); } catch (e) { if (!(e instanceof PoisonError)) throw e; } @@ -7493,10 +7436,7 @@ export class Lowerer { // instances of its own; the joint fixpoint covers both). while (es < this.emitSpecQueue.length) { try { - const spec = this.emitSpecQueue[es++]!; - const fn = this.withClosureRoot(emitSpecRoot(spec), () => - lowerEmitOverrideSpec(this, spec), - ); + const fn = lowerEmitOverrideSpec(this, this.emitSpecQueue[es++]!); if (fn) functions.push(fn); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -8050,9 +7990,7 @@ export class Lowerer { this.genericClassDemandPriority.set(info, ref); instanceFunctions.push( ...this.withGenericDemandOwner(owner, () => - this.shapes.withDeclaredOrderPriority(ref, () => - this.withClosureRoot(info.def.name, () => this.lowerClassMembers(info)), - ), + this.shapes.withDeclaredOrderPriority(ref, () => this.lowerClassMembers(info)), ), ); relieveHeapPressure(); @@ -8079,7 +8017,7 @@ export class Lowerer { instanceFunctions.push( this.withGenericDemandOwner(owner, () => this.shapes.withDeclaredOrderPriority(ref, () => - this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), + this.lowerGenericInstance(info, inst), ), ), ); @@ -8112,12 +8050,9 @@ export class Lowerer { ); while (specLowered < waveEnd) { try { - const fn = this.shapes.withDeclaredOrderPriority([4, instanceMetadataOrder++], () => { - const spec = this.emitSpecQueue[specLowered++]!; - return this.withClosureRoot(emitSpecRoot(spec), () => - lowerEmitOverrideSpec(this, spec), - ); - }); + const fn = this.shapes.withDeclaredOrderPriority([4, instanceMetadataOrder++], () => + lowerEmitOverrideSpec(this, this.emitSpecQueue[specLowered++]!), + ); if (fn) instanceFunctions.push(fn); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -8145,9 +8080,7 @@ export class Lowerer { initFunctions.push( this.withGenericDemandOwner(owner, () => this.shapes.withDeclaredOrderPriority(priority, () => - this.withClosureRoot(this.initNameOf.get(fp.sf)!, () => - this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!), - ), + this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!), ), ), ); @@ -8173,9 +8106,7 @@ export class Lowerer { const priority = metadataPriority.get(name) ?? [3, expressionMetadataOrder++]; const owner = demandOwner(priority); const fn = this.withGenericDemandOwner(owner, () => - this.shapes.withDeclaredOrderPriority(priority, () => - this.withClosureRoot(name, unit.lower), - ), + this.shapes.withDeclaredOrderPriority(priority, unit.lower), ); if (fn) loweredUnits.set(name, fn); relieveHeapPressure(); @@ -13558,7 +13489,7 @@ export class Lowerer { const key = `${fn.valueFn ?? fn.fn}:${typeKey(funcType)}:${adapterShape}`; let fnName = this.builtinCallableValueFns.get(key); if (!fnName) { - fnName = keyedHelperName(this, `%builtin.value.${fn.valueFn ?? fn.fn}`, key); + fnName = `%builtin.value.${fn.valueFn ?? fn.fn}.${this.builtinCallableValueFns.size}`; this.builtinCallableValueFns.set(key, fnName); this.builtinCallableValueNames.set(fnName, display.slice(display.lastIndexOf(".") + 1)); const params = funcType.params.map((type, index) => ({ diff --git a/packages/compiler/src/frontend/lowering/native-ffi.ts b/packages/compiler/src/frontend/lowering/native-ffi.ts index 62f2be9f1f..0f932ba930 100644 --- a/packages/compiler/src/frontend/lowering/native-ffi.ts +++ b/packages/compiler/src/frontend/lowering/native-ffi.ts @@ -562,7 +562,7 @@ export function lowerFfiMemoryModule(lowerer: Lowerer, loc: SrcLoc): IrExpr { const entries = lowerer.ffiImports .filter((entry) => entry.library !== undefined) .map((entry) => { - const name = lowerer.closureName(loc, "", "%ffi.module"); + const name = `%ffi.module.${lowerer.lambdaCounter++}`; const sourceTypes = ffiSourceParamTypes(entry.params); const locals: IrLocal[] = sourceTypes.map((_, i) => ({ id: `%arg${i}`, diff --git a/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts b/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts index 388ac3904f..855da3bda1 100644 --- a/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts +++ b/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { BOOL, STRING, @@ -28,7 +27,7 @@ export function objectEnumerationReceiver( const key = `obj.receiver:${source.unionId}:${expected.shapeId}`; let name = lowerer.arrHofHelpers.get(key); if (name === undefined) { - name = keyedHelperName(lowerer, "%obj.receiver", key); + name = `%obj.receiver.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const value = varRef("value.0", source, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/pairs-snapshot.ts b/packages/compiler/src/frontend/lowering/pairs-snapshot.ts index acf7b3bf9b..3b33fb0cde 100644 --- a/packages/compiler/src/frontend/lowering/pairs-snapshot.ts +++ b/packages/compiler/src/frontend/lowering/pairs-snapshot.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { arrayOf, BOOL, @@ -50,7 +49,7 @@ export function pairsSnapshotHelper( const key = `${options.keyPrefix}.snapshot:${shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = keyedHelperName(lowerer, `%${options.keyPrefix}.snapshot`, key); + const name = `%${options.keyPrefix}.snapshot.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, name); const recordType: IrType = { kind: "record", shapeId }; const pairsType = arrayOf(STRING); diff --git a/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts index 84aa8dac09..09d228b05e 100644 --- a/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts +++ b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { varRef } from "../../ir/build.js"; import { type IrExpr, @@ -41,7 +40,7 @@ export function adaptPromiseExecutor( const key = `promise.executor:${typeKey(execT)}:${typeKey(inner)}`; let name = lowerer.arrHofHelpers.get(key); if (name === undefined) { - name = keyedHelperName(lowerer, "%promise.executor", key); + name = `%promise.executor.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; const captured: IrLocal = { id: "f.0", name: "f", type: execT, mutable: false, boxed: true }; diff --git a/packages/compiler/src/frontend/lowering/require-initialization.ts b/packages/compiler/src/frontend/lowering/require-initialization.ts index c8fc9c7309..d979423b0e 100644 --- a/packages/compiler/src/frontend/lowering/require-initialization.ts +++ b/packages/compiler/src/frontend/lowering/require-initialization.ts @@ -9,7 +9,6 @@ * and every reference that can run earlier checks it first — the same * module.tdzCheck the import-cycle bindings use. A reference whose lowering * leaves no expression to check keeps the SC1013 fence. */ -import { positionTag } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { @@ -120,7 +119,7 @@ export function markRequireEarlyBindings(lowerer: Lowerer): void { const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, ""); const bySymbol = new Map(); for (const { decl } of declared) { - const flagId = `%g.${tag}%required.${positionTag(lowerer, decl)}`; + const flagId = `%g.${tag}%required.${ts.tokenStartOfNode(decl)}`; lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true }); const flag: RequireInitFlag = { flagId, names: new Map() }; for (const id of boundNames(decl.name)) { diff --git a/packages/compiler/src/frontend/lowering/stable-names.ts b/packages/compiler/src/frontend/lowering/stable-names.ts deleted file mode 100644 index 8f7d0967e4..0000000000 --- a/packages/compiler/src/frontend/lowering/stable-names.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { InternalCompilerError } from "../../errors.js"; - -/** The owner of the names keyedHelperName hands out. */ -export interface KeyedNameOwner { - /** Each keyed name with a second hash of the key it was made from, which - * tells a repeated key from a different key with the same name. */ - readonly keyedNameKeys: Map; -} - -/** 48 bits of two FNV-1a hashes of `key`, as 12 hex digits. */ -function keyHash(key: string): string { - let a = 0x811c9dc5; - let b = 0x01000193 ^ 0x5bd1e995; - for (let i = 0; i < key.length; i++) { - const c = key.charCodeAt(i); - a = Math.imul(a ^ c, 0x01000193); - b = Math.imul(b ^ c, 0x01000193) ^ (b >>> 13); - } - const high = (b >>> 0).toString(16).padStart(8, "0").slice(4); - return high + (a >>> 0).toString(16).padStart(8, "0"); -} - -/** A 32-bit hash of `key` independent of keyHash's bits. */ -function checkHash(key: string): number { - let h = 0x9e3779b9; - for (let i = 0; i < key.length; i++) - h = Math.imul(h ^ key.charCodeAt(i), 0x85ebca6b) ^ (h >>> 15); - return h >>> 0; -} - -/** The name of a helper interned by `key`: ``. It depends only on the key, not on how many helpers were interned - * before, so a helper has the same name whichever unit first needs it. Two - * keys that hash to the same name throw an InternalCompilerError. */ -export function keyedHelperName( - owner: KeyedNameOwner, - prefix: string, - key: string, - separator = ".", -): string { - const name = `${prefix}${separator}${keyHash(key)}`; - const check = checkHash(key); - const previous = owner.keyedNameKeys.get(name); - if (previous === undefined) owner.keyedNameKeys.set(name, check); - else if (previous !== check) - throw new InternalCompilerError(`helper name ${name} is shared by two helper keys`); - return name; -} diff --git a/packages/compiler/src/frontend/lowering/strict-equality.ts b/packages/compiler/src/frontend/lowering/strict-equality.ts index 8a87c8111b..7b9b6902b6 100644 --- a/packages/compiler/src/frontend/lowering/strict-equality.ts +++ b/packages/compiler/src/frontend/lowering/strict-equality.ts @@ -1,4 +1,3 @@ -import { keyedHelperName } from "./stable-names.js"; import { boolLit, varRef } from "../../ir/build.js"; import { BOOL, @@ -136,7 +135,7 @@ export function lowerUnionEquality( loc, ); if (!result) return null; - name = keyedHelperName(lowerer, "%value.eq", key); + name = `%value.eq.${lowerer.equalityHelpers.size}`; lowerer.equalityHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/structural-positions.ts b/packages/compiler/src/frontend/lowering/structural-positions.ts deleted file mode 100644 index ea8535d9ef..0000000000 --- a/packages/compiler/src/frontend/lowering/structural-positions.ts +++ /dev/null @@ -1,107 +0,0 @@ -import * as ts from "../ts7/adapter.js"; -import { keyedHelperName, type KeyedNameOwner } from "./stable-names.js"; - -/** The declaration kinds whose names anchor the positions inside them. */ -const ANCHOR_KINDS: ReadonlySet = new Set([ - ts.SyntaxKind.FunctionDeclaration, - ts.SyntaxKind.ClassDeclaration, - ts.SyntaxKind.ClassExpression, - ts.SyntaxKind.VariableDeclaration, - ts.SyntaxKind.MethodDeclaration, - ts.SyntaxKind.GetAccessor, - ts.SyntaxKind.SetAccessor, - ts.SyntaxKind.PropertyDeclaration, - ts.SyntaxKind.PropertyAssignment, - ts.SyntaxKind.ModuleDeclaration, - ts.SyntaxKind.EnumDeclaration, - ts.SyntaxKind.Constructor, - ts.SyntaxKind.InterfaceDeclaration, - ts.SyntaxKind.TypeAliasDeclaration, -]); - -/** The name of `node` when it is a named declaration that anchors the - * positions of the nodes inside it, or null. */ -function anchorName(node: ts.Node): string | null { - return ts.declarationNameAt(node, node.index, ANCHOR_KINDS); -} - -/** The nearest enclosing anchor of `node` (not `node` itself), or its - * source file. */ -function anchorOf(node: ts.Node): ts.Node { - for (let parent = node.parent; parent !== undefined; parent = parent.parent) { - if (ts.isSourceFile(parent) || anchorName(parent) !== null) return parent; - } - return ts.getSourceFileOfNode(node); -} - -/** Source positions that stay the same when code outside the enclosing - * declaration changes. A node's structural position is its file, the chain - * of enclosing named declarations (each with its kind, its name and its - * ordinal among the declarations of the same kind and name in its own - * anchor), and its ordinal among the nodes of its kind whose nearest anchor - * is the same declaration. Editing another declaration, or code nested in another - * named declaration of the same anchor, leaves the position unchanged. */ -export class StructuralPositions { - /** Per anchor and key: the indices of the nodes with that key whose - * nearest anchor it is, in preorder (anchoredNodes). */ - private readonly nodesByKey = new Map>(); - private readonly paths = new Map(); - - /** The ordinal of `node` among the nodes of `anchor` with its key. */ - private ordinalIn(anchor: ts.Node, node: ts.Node, key: string): number { - let byKey = this.nodesByKey.get(anchor); - if (byKey === undefined) this.nodesByKey.set(anchor, (byKey = new Map())); - let indices = byKey.get(key); - if (indices === undefined) { - indices = ts.anchoredNodes(anchor, key, ANCHOR_KINDS); - byKey.set(key, indices); - } - // Indices increase in preorder. - let low = 0; - let high = indices.length - 1; - while (low <= high) { - const middle = (low + high) >> 1; - const at = indices[middle]!; - if (at === node.index) return middle; - if (at < node.index) low = middle + 1; - else high = middle - 1; - } - return -1; - } - - /** The anchor chain of `anchor` from the file down. */ - private pathOf(anchor: ts.Node): string { - const cached = this.paths.get(anchor); - if (cached !== undefined) return cached; - let path: string; - if (ts.isSourceFile(anchor)) path = anchor.fileName; - else { - const parent = anchorOf(anchor); - // The kind keeps declarations of one name apart: a getter and its - // setter, or a class and the namespace merged with it. - const key = `${anchor.kind}:${anchorName(anchor)}`; - path = `${this.pathOf(parent)}/${key}#${this.ordinalIn(parent, anchor, key)}`; - } - this.paths.set(anchor, path); - return path; - } - - /** The structural position of `node`. */ - of(node: ts.Node): string { - const anchor = anchorOf(node); - const name = anchorName(node); - const key = name === null ? `${node.kind}` : `${node.kind}:${name}`; - return `${this.pathOf(anchor)}|${key}#${this.ordinalIn(anchor, node, key)}`; - } -} - -/** The owner of structural positions and the names made from them. */ -export interface PositionTagOwner extends KeyedNameOwner { - readonly positions: StructuralPositions; -} - -/** A short tag for `node`'s structural position, for names that must stay - * unique per source node (class expressions, namespace blocks). */ -export function positionTag(owner: PositionTagOwner, node: ts.Node): string { - return keyedHelperName(owner, "", owner.positions.of(node), ""); -} diff --git a/packages/compiler/src/frontend/ts7/ast.ts b/packages/compiler/src/frontend/ts7/ast.ts index 0bddd1a807..369213d896 100644 --- a/packages/compiler/src/frontend/ts7/ast.ts +++ b/packages/compiler/src/frontend/ts7/ast.ts @@ -47,9 +47,8 @@ import { } from "./ast-guards.generated.js"; import type { Ts7DiagnosticData as Diagnostic } from "./session-schema.generated.js"; import { ModifierFlags, SyntaxKind } from "./enums.js"; -import { AstKind, KIND_NODE_LIST } from "./ast-schema.generated.js"; +import { AstKind } from "./ast-schema.generated.js"; import { PREORDER_LIST } from "./ast-wire.js"; -import { tokenStartOfNode } from "./node-functions.js"; /* ---- everything 7 kept under the same name ---- */ @@ -197,93 +196,6 @@ function walkLinkedPreorder( } } -/** The semantic children of a wire node: list items stand in for their - * list, and attached JSDoc trees are left out (the walkPreorder view). */ -function semanticChildIndices(root: Node, index: number, out: number[]): void { - const wire = root.file.wire; - for (let child = wire.firstChild(index); child !== 0; child = wire.next(child)) { - const kind = wire.kind(child); - if (kind === KIND_NODE_LIST) { - for (let item = wire.firstChild(child); item !== 0; item = wire.next(item)) out.push(item); - } else if (kind !== AstKind.JSDoc) out.push(child); - } -} - -/** The outermost node of `sourceFile` whose token span is exactly - * [start, end), or undefined. Only nodes ending at `end` are - * materialized. */ -export function nodeAtSpan(sourceFile: SourceFile, start: number, end: number): Node | undefined { - const wire = sourceFile.file.wire; - const children: number[] = []; - let index = sourceFile.index; - for (;;) { - children.length = 0; - semanticChildIndices(sourceFile, index, children); - let next = 0; - for (const child of children) { - if (wire.pos(child) > start || wire.end(child) < end) continue; - if (wire.end(child) === end) { - const node = sourceFile.file.node(child); - if (tokenStartOfNode(node) === start) return node; - } - next = child; - break; - } - if (next === 0) return undefined; - index = next; - } -} - -/** The name of the declaration at wire `index` in `root`'s file when its - * kind is in `kinds`: its identifier, private identifier or string literal - * name, "constructor" for a constructor, or null. Reads the wire without - * materializing nodes. */ -export function declarationNameAt( - root: Node, - index: number, - kinds: ReadonlySet, -): string | null { - const wire = root.file.wire; - const kind = wire.kind(index); - if (!kinds.has(kind)) return null; - if (kind === SyntaxKind.Constructor) return "constructor"; - const name = wire.namedChild(index, "name"); - if (name === 0) return null; - const nameKind = wire.kind(name); - if ( - nameKind !== SyntaxKind.Identifier && - nameKind !== SyntaxKind.PrivateIdentifier && - nameKind !== SyntaxKind.StringLiteral - ) - return null; - return wire.text(name) ?? null; -} - -/** The indices, in preorder, of the nodes below `root` that have `key` and - * are not inside a nested named declaration (declarationNameAt with - * `kinds`). A named declaration is keyed `:` and its subtree is - * not visited; other nodes are keyed by their kind. No node is - * materialized. */ -export function anchoredNodes(root: Node, key: string, kinds: ReadonlySet): number[] { - const wire = root.file.wire; - const out: number[] = []; - const children: number[] = []; - const stack: number[] = []; - semanticChildIndices(root, root.index, children); - for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]!); - while (stack.length > 0) { - const index = stack.pop()!; - const kind = wire.kind(index); - const name = declarationNameAt(root, index, kinds); - if ((name === null ? String(kind) : `${kind}:${name}`) === key) out.push(index); - if (name !== null) continue; - children.length = 0; - semanticChildIndices(root, index, children); - for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]!); - } - return out; -} - export function getLineAndCharacterOfPosition( sourceFile: SourceFile, position: number, diff --git a/packages/compiler/src/frontend/type-mapper.test.ts b/packages/compiler/src/frontend/type-mapper.test.ts index 163ba7b9ee..cf78554b42 100644 --- a/packages/compiler/src/frontend/type-mapper.test.ts +++ b/packages/compiler/src/frontend/type-mapper.test.ts @@ -41,7 +41,7 @@ describe("union arm lookup", () => { test("sees finalized recursive arms after a missing lookup", () => { const unions = new UnionRegistry(); const recursive = {} as Type; - const union = unions.recursiveRef(recursive, () => "Recursive"); + const union = unions.recursiveRef(recursive); expect(unions.armTag(union, STRING)).toBe(-1); unions.finalizeRecursive(recursive, [ F64, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 79091cadcc..a233f0186d 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1,5 +1,4 @@ import { InternalCompilerError } from "../errors.js"; -import { keyedHelperName, type KeyedNameOwner } from "./lowering/stable-names.js"; import { mockCallRecordType, mockContextType } from "./mock-shapes.js"; import * as ts from "./ts7/adapter.js"; import { bodyReadsArguments } from "./arguments-usage.js"; @@ -253,8 +252,6 @@ function comparePriority(left: DeclaredOrderPriority, right: DeclaredOrderPriori export class ShapeRegistry { /** Existing layouts change only when a recursive placeholder closes. */ revision = 0; - /** Each shape id with a second hash of its key (keyedHelperName). */ - readonly keyedNameKeys = new Map(); private readonly byKey = new Map(); private readonly byId = new Map(); /** Historical emit-order rank of the declaration-order metadata each @@ -273,7 +270,7 @@ export class ShapeRegistry { * writer after all priority candidates have closed. */ private readonly derivedDeclaredOrderFinalizers: (() => void)[] = []; private currentDeclaredOrderPriority: DeclaredOrderPriority = [0, 0]; - /** All interned shapes in first-seen order. */ + /** All interned shapes in first-seen (`r0`, `r1`, ...) order. */ readonly shapes: IrRecordShape[] = []; /** ts.Types currently being mapped — a BACK-REFERENCE to one of these is * the recursive knot (`interface TreeNode { children: TreeNode[] }`): @@ -393,12 +390,11 @@ export class ShapeRegistry { /** The shape id a back-reference to an in-progress type resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER - * entry (empty fields) the outer frame finalizes. `describe` names a new - * placeholder (recursiveTypeName). */ - recursiveRef(t: ts.Type, describe: () => string): string { + * entry (empty fields) the outer frame finalizes. */ + recursiveRef(t: ts.Type): string { let id = this.recIds.get(t); if (id === undefined) { - id = recursivePlaceholderName(this, "r", describe(), (name) => this.byId.has(name)); + id = `r${this.shapes.length}`; const shape: IrRecordShape = { id, fields: [] }; this.byId.set(id, shape); this.shapes.push(shape); @@ -467,7 +463,7 @@ export class ShapeRegistry { const key = this.keyOf(fields, tuple, indexValue); let id = this.byKey.get(key); if (id === undefined) { - id = keyedHelperName(this, "r", key, ""); + id = `r${this.shapes.length}`; const shape: IrRecordShape = { id, fields, @@ -545,57 +541,6 @@ export class ShapeRegistry { } } -/** The id of a recursive placeholder described by `description`. Two - * different types can still share a description (recursiveTypeName stops - * after a few levels of type arguments); the later one takes the first free - * name with a numbered suffix. */ -function recursivePlaceholderName( - owner: KeyedNameOwner, - prefix: string, - description: string, - taken: (name: string) => boolean, -): string { - let name = keyedHelperName(owner, prefix, `recursive:${description}`, ""); - for (let n = 1; taken(name); n++) - name = keyedHelperName(owner, prefix, `recursive:${description}#${n}`, ""); - return name; -} - -/** How many levels of type arguments recursiveTypeName describes. */ -const RECURSIVE_NAME_DEPTH = 3; - -/** A recursive checker type's description, which names its placeholder - * whichever frame first reaches it: its printed form, the structural - * position of its first declaration, and the same for its type arguments - * (two instantiations print the same when their type arguments are type - * parameters or classes of the same name from different declarations). */ -function recursiveTypeName( - t: ts.Type, - ctx: Pick, - depth = 0, -): string { - const symbol = t.getAliasSymbol() ?? t.getSymbol(); - const declaration = symbol?.declarations[0]; - const where = declaration - ? (ctx.positionOf?.(declaration) ?? - `${ts.getSourceFileOfNode(declaration).fileName}:${declaration.pos}`) - : ""; - let description = `${ctx.checker.typeToString(t)}@${where}`; - if (depth < RECURSIVE_NAME_DEPTH) { - const aliasArguments = - t.getAliasSymbol() !== undefined ? (t.getAliasTypeArguments() ?? []) : []; - const typeArguments = - aliasArguments.length > 0 - ? aliasArguments - : t.isTypeReference() - ? ctx.checker.getTypeArguments(t) - : []; - if (typeArguments.length > 0) - description += `<${typeArguments.map((arg) => recursiveTypeName(arg, ctx, depth + 1)).join(",")}>`; - } - return description; -} - /** The frontend's union interner — mirrors ShapeRegistry. A union's * canonical identity includes its typeKey-sorted arms and literal * discriminator contract. An arm's index in that list IS its runtime tag. @@ -603,12 +548,10 @@ function recursiveTypeName( export class UnionRegistry { /** Existing arm/discriminator contracts change when a placeholder closes. */ revision = 0; - /** Each union id with a second hash of its key (keyedHelperName). */ - readonly keyedNameKeys = new Map(); private readonly byKey = new Map(); private readonly byId = new Map(); private readonly armTags = new Map>(); - /** All interned unions in first-seen order. */ + /** All interned unions in first-seen (`u0`, `u1`, ...) order. */ readonly unions: IrUnionDef[] = []; /** ts.Types currently being mapped — a back-reference to one is the * recursive knot passing through a union (`type Tree = Leaf | Branch` @@ -624,12 +567,11 @@ export class UnionRegistry { /** The union id a back-reference to an in-progress union resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER - * entry (empty arms) the outer frame finalizes. `describe` names a new - * placeholder (recursiveTypeName). */ - recursiveRef(t: ts.Type, describe: () => string): string { + * entry (empty arms) the outer frame finalizes. */ + recursiveRef(t: ts.Type): string { let id = this.recIds.get(t); if (id === undefined) { - id = recursivePlaceholderName(this, "u", describe(), (name) => this.byId.has(name)); + id = `u${this.unions.length}`; const def: IrUnionDef = { id, arms: [] }; this.byId.set(id, def); this.unions.push(def); @@ -679,7 +621,7 @@ export class UnionRegistry { const key = JSON.stringify([arms.map(typeKey), discriminant]); let id = this.byKey.get(key); if (id === undefined) { - id = keyedHelperName(this, "u", key, ""); + id = `u${this.unions.length}`; const def: IrUnionDef = { id, arms, ...(discriminant ? { discriminant } : {}) }; this.byKey.set(key, id); this.byId.set(id, def); @@ -1173,10 +1115,6 @@ export interface TypeMapperCtx { * empty object type rule in the object mapping). */ emptyLiteralArm?: boolean; checker: ts.TypeChecker; - /** The structural position of a declaration (structural-positions.ts), - * which names recursive placeholders independently of where unrelated - * code sits. The Lowerer supplies it. */ - positionOf?: (node: ts.Node) => string; /** JavaScript declarations whose instance or object-literal types only * the checked-dynamic representation holds (js-dynamic-objects.ts). * Absent when the program has none: every mapping is unchanged. */ @@ -4713,10 +4651,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const knownRecursive = unions.recursiveUnionFor(widened); if (knownRecursive !== undefined) return { kind: "union", unionId: knownRecursive }; if (unions.inProgress.has(widened)) { - return { - kind: "union", - unionId: unions.recursiveRef(widened, () => recursiveTypeName(widened, ctx)), - }; + return { kind: "union", unionId: unions.recursiveRef(widened) }; } unions.inProgress.add(widened); const sensitivityAtEntry = contextResolutions; @@ -7060,10 +6995,7 @@ function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { // frame completes (finalizeRecursive below). Backends represent records // as heap references, so the self-reference is an ordinary pointer. if (shapes.inProgress.has(widened)) { - return { - kind: "record", - shapeId: shapes.recursiveRef(widened, () => recursiveTypeName(widened, ctx)), - }; + return { kind: "record", shapeId: shapes.recursiveRef(widened) }; } shapes.inProgress.add(widened); const sensitivityAtEntry = contextResolutions; diff --git a/packages/compiler/test/array-pipeline-fusion.test.ts b/packages/compiler/test/array-pipeline-fusion.test.ts index 55cd0025ec..f5d743265f 100644 --- a/packages/compiler/test/array-pipeline-fusion.test.ts +++ b/packages/compiler/test/array-pipeline-fusion.test.ts @@ -26,7 +26,7 @@ function helperCalls(fn: IrFunction): string[] { everyStmtList(fn.body, { expr: (e) => { if (e.kind === "call" && e.callee.startsWith("%arr.")) - calls.push(e.callee.replace(/\.[0-9a-f]+$/, "")); + calls.push(e.callee.replace(/\.\d+$/, "")); return true; }, stmt: () => true, diff --git a/packages/compiler/test/format-float-arguments.test.ts b/packages/compiler/test/format-float-arguments.test.ts index 6ce9c4bc04..e59e023ca7 100644 --- a/packages/compiler/test/format-float-arguments.test.ts +++ b/packages/compiler/test/format-float-arguments.test.ts @@ -27,7 +27,7 @@ test("util.format %f lowers a boolean argument once", async () => { const helpers = mod.functions .map((f) => f.name) .filter((name) => name.startsWith("%arr.")) - .map((name) => name.replace(/\.[0-9a-f]+$/, "")) + .map((name) => name.replace(/\.\d+$/, "")) .sort(); expect(helpers).toEqual(["%arr.every", "%arr.some"]); } finally { diff --git a/packages/compiler/test/in-runtime-key-emission.test.ts b/packages/compiler/test/in-runtime-key-emission.test.ts index 589685a7a8..aebbb2ea4f 100644 --- a/packages/compiler/test/in-runtime-key-emission.test.ts +++ b/packages/compiler/test/in-runtime-key-emission.test.ts @@ -55,7 +55,7 @@ const d: Record = {}; d["a"] = 1; console.log(has(d, "a"), has(d, "b")); `); - const helper = body(llvm, /sc_b?f__x25_rec_haskey_[0-9a-f]+/); + const helper = body(llvm, /sc_b?f__x25_rec_haskey_\d+/); expect(helper).toContain("@scr_map_has_str("); // Not a scan over a fresh key snapshot per probe. expect(helper).not.toContain("@scr_map_keys_js_order("); diff --git a/packages/compiler/test/stable-names.test.ts b/packages/compiler/test/stable-names.test.ts deleted file mode 100644 index 720472556d..0000000000 --- a/packages/compiler/test/stable-names.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { expect, test } from "vitest"; -import { compile, deserializeModule, type IrModule } from "../src/index.js"; - -/** Lowers each source in turn as the same file, so names that include the - * file's path compare equal. */ -async function lower(...sources: string[]): Promise { - const dir = await mkdtemp(join(tmpdir(), "scriptc-stable-names-")); - try { - const entry = join(dir, "main.ts"); - const outPath = join(dir, "main.ir.json"); - const modules: IrModule[] = []; - for (const source of sources) { - await writeFile(entry, source); - const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); - if (!result.ok) - throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); - modules.push(deserializeModule(await readFile(outPath, "utf8"))); - } - return modules; - } finally { - await rm(dir, { recursive: true, force: true }); - } -} - -function names(mod: IrModule): string[] { - return [ - ...mod.records.map((record) => record.id), - ...mod.unions.map((union) => union.id), - ...mod.functions.map((fn) => fn.name), - ...mod.classes.map((cls) => cls.name), - ].sort(); -} - -const recursive = ` -type Tree = { value: number; children: Tree[]; parent: Tree | null }; -function total(tree: Tree): number { - let sum = tree.value; - for (const child of tree.children) sum += total(child); - return sum; -} -const root: Tree = { value: 1, children: [], parent: null }; -root.children.push({ value: 2, children: [], parent: root }); -console.log(total(root)); -`; - -test("recursive record and union ids do not depend on the code above the type", async () => { - const [before, after] = await lower( - recursive, - `// A comment above the type.\n\ntype Unrelated = { name: string };\n\n${recursive}`, - ); - expect(before.records.some((record) => record.fields.some((f) => f.name === "children"))).toBe( - true, - ); - expect(names(after)).toEqual(names(before)); -}); - -test("recursive instantiations that print alike get distinct placeholders", async () => { - const [mod] = await lower(` -namespace A { - export class Item { - a = 1; - } -} -namespace B { - export class Item { - b = "b"; - } -} -type Chain = { item: T; next: Chain | null }; -const first: Chain = { item: new A.Item(), next: null }; -const second: Chain = { item: new B.Item(), next: null }; -first.next = { item: new A.Item(), next: null }; -second.next = { item: new B.Item(), next: null }; -console.log(first.next.item.a, second.next.item.b); -`); - const chains = mod.records.filter((record) => record.fields.some((f) => f.name === "next")); - expect(new Set(chains.map((record) => record.id)).size).toBe(chains.length); - expect(chains.length).toBeGreaterThanOrEqual(2); -}); - -test("a getter and a setter of one name keep their local classes apart", async () => { - const [mod] = await lower(` -class Box { - private v = 0; - get value(): number { - class Local { - n(): number { - return 1; - } - } - return new Local().n() + this.v; - } - set value(x: number) { - class Local { - n(): number { - return 2; - } - } - this.v = x * new Local().n(); - } -} -const box = new Box(); -box.value = 5; -console.log(box.value); -`); - const locals = mod.classes.filter((cls) => cls.name.includes("Local")); - expect(locals).toHaveLength(2); - expect(locals[0]!.name).not.toBe(locals[1]!.name); -}); diff --git a/packages/compiler/test/type-mapper-unions.test.ts b/packages/compiler/test/type-mapper-unions.test.ts index 6849600028..c714df4d75 100644 --- a/packages/compiler/test/type-mapper-unions.test.ts +++ b/packages/compiler/test/type-mapper-unions.test.ts @@ -139,8 +139,7 @@ describe("checker and synthesized optional union identity", () => { nullable.arms.filter((arm) => arm.kind !== "nullT" && arm.kind !== "undefinedT"), ), ).toBe(required.unionId); - // Cases follow the arms, which are ordered by their type keys. - expect(nullable.discriminant!.cases.map((entry) => entry.values).sort()).toEqual([ + expect(nullable.discriminant!.cases.map((entry) => entry.values)).toEqual([ ["a", "alias"], ["b"], ]); From d426960f454e86bc8f53226e59b5a4db256f317c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 01:55:38 -0500 Subject: [PATCH 073/395] Make Cipheriv and Decipheriv Transforms and accept function stages anywhere in pipeline() Cipheriv and Decipheriv values are Transforms, as in Node: they can be written and ended, piped into, used as pipeline() and stream/promises pipeline stages, composed, read, iterated with for await, read by stream/consumers, passed to finished(), and listened to, and their stream properties read as in Node. Each chunk goes through the cipher's update(), and the end pushes final() or reports its error through 'error'; writing after final() throws out of write(). The stream unit builds a cipher's Transform on its first stream use from operations the cipher's handle provides (ScrDynHandleOps.transform), so programs that use ciphers without streams link as before. The cipher is a collector-headered object behind a traced handle: its Transform's steps hold it, and the pair is reclaimed as a cycle. Through untyped values, in a program that uses streams, a cipher's stream properties and its on, once, off, pipe, unpipe, read, pause, resume, setEncoding, write, end, destroy, cork and uncork members run on its Transform; other stream members throw a coded SC2020 error. A Transform holds a write's callback while its readable side is at the high-water mark and runs it at the next read, and runs it on a tick once the readable side has ended, as Transform._write does. write() on a PassThrough, a Transform, a Hash or a cipher used to return true however much the readable side buffered. zlib streams applied the rule already and keep their timing. pipeline() accepts function stages anywhere among its streams, so runs of async generator stages can sit on both sides of a stream. A stage can be named by a function declaration, or a const bound to a function expression, that nothing reassigns; its body lowers again at the pipeline with its source typed by the pipeline. The source can also be an array, a string, a synchronous generator object, or a synchronous generator function without parameters: an async generator steps the source's iterator and yields each element, and the next stage receives each element on the same tick as with Node's for await over the source. The runtime's function-stage entry points take a bitmask of the function positions and one (next, read, return) helper triple for each generator pumped into a stream, so RUNTIME_ABI_VERSION is now 25. In JavaScript, the chunks a stage receives from the previous stage's generator read as the strings or Buffers it yields, so their string and Buffer methods compile. Corpus 7055-7059 cover cipher streams in TypeScript and JavaScript, their errors, close order and backpressure, the pipeline stages and sources with their tick order, and Transform backpressure. --- .../limitations/builtin-apis/crypto.mdx | 5 +- .../builtin-apis/streams-and-readline.mdx | 8 +- .../src/generated/node-v24-compatibility.json | 24 +- .../generated/node-v24-internal.json | 45 ++ internal/compatibility/src/generate.mjs | 2 +- internal/compatibility/static-support.json | 6 +- packages/cli/test/native-link-info.test.ts | 4 +- .../ambient/scriptc-node-fallback.d.ts | 7 +- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../compiler/src/backend/llvm/lib-stream.ts | 29 +- 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/binding-analysis.ts | 10 +- .../src/frontend/lowering/builtins/crypto.ts | 5 +- .../frontend/lowering/builtins/hash-stream.ts | 133 +++++- .../src/frontend/lowering/containers/bytes.ts | 11 +- .../lowering/containers/iterator-helpers.ts | 4 +- .../src/frontend/lowering/dyn-streams.ts | 2 +- .../lowering/expressions/property-access.ts | 3 + .../src/frontend/lowering/lower-stmts.ts | 10 +- .../src/frontend/lowering/lower-stream.ts | 424 ++++++++++++++---- .../compiler/src/frontend/lowering/lowerer.ts | 15 +- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 35 +- packages/compiler/src/ir/runtime-features.ts | 1 + packages/compiler/src/ir/validate.ts | 33 +- .../test/native-codegen-integration.test.ts | 4 +- .../tests/corpus/7055-cipher-streams.ts.json | 6 + .../corpus/7056-cipher-stream-errors.ts.json | 6 + .../tests/corpus/7057-cipher-streams.mjs.json | 6 + .../7058-pipeline-function-stages.mjs.json | 6 + .../7059-transform-backpressure.ts.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_cipher.c | 132 +++++- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_dyn.h | 29 ++ packages/runtime/src/scr_json.c | 3 + packages/runtime/src/scr_stream.c | 386 ++++++++++++---- packages/runtime/src/scr_stream.h | 64 +-- packages/runtime/src/scr_stream_dyn.c | 5 +- packages/runtime/src/scr_zlib_stream.c | 3 +- tests/corpus/7055-cipher-streams.ts | 182 ++++++++ tests/corpus/7056-cipher-stream-errors.ts | 111 +++++ tests/corpus/7057-cipher-streams.mjs | 85 ++++ .../corpus/7058-pipeline-function-stages.mjs | 180 ++++++++ tests/corpus/7059-transform-backpressure.ts | 90 ++++ .../corpus-costs/7055-cipher-streams.ts.json | 1 + .../7056-cipher-stream-errors.ts.json | 1 + .../corpus-costs/7057-cipher-streams.mjs.json | 1 + .../7058-pipeline-function-stages.mjs.json | 1 + .../7059-transform-backpressure.ts.json | 1 + .../self-hosting-native-toolchain.test.ts | 2 +- 55 files changed, 1803 insertions(+), 342 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7055-cipher-streams.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7056-cipher-stream-errors.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7057-cipher-streams.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7058-pipeline-function-stages.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7059-transform-backpressure.ts.json create mode 100644 tests/corpus/7055-cipher-streams.ts create mode 100644 tests/corpus/7056-cipher-stream-errors.ts create mode 100644 tests/corpus/7057-cipher-streams.mjs create mode 100644 tests/corpus/7058-pipeline-function-stages.mjs create mode 100644 tests/corpus/7059-transform-backpressure.ts create mode 100644 tests/harness/corpus-costs/7055-cipher-streams.ts.json create mode 100644 tests/harness/corpus-costs/7056-cipher-stream-errors.ts.json create mode 100644 tests/harness/corpus-costs/7057-cipher-streams.mjs.json create mode 100644 tests/harness/corpus-costs/7058-pipeline-function-stages.mjs.json create mode 100644 tests/harness/corpus-costs/7059-transform-backpressure.ts.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 3c830c63d9..9ca6bfff4a 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -8,7 +8,10 @@ `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, cipher streams, and wasm32-wasi builds are refused. + `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. + 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. Decrypting chacha20-poly1305 without an authentication tag fails in `final()`; Node 24.22 returns unverified plaintext. After `update()` fails, for example past the AES-GCM limit of 2^36 - 32 bytes per message, every later call on that cipher throws; Node still lets `final()` run. An output encoding that is a function or an instance of a class defining `toString` throws a coded SC2020 error. diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index a21f7a617f..b7c2e062ef 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -3,14 +3,16 @@ `readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream. When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects. Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol. - A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams at run time; other values throw a not-supported error. + A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error. Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one. `Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported. In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported. Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported. - `pipeline` accepts one run of function stages: async generator functions between streams (`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 })`), 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, several runs of function stages, and function stages together with pipeline options are unsupported. + 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. `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. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 66b0ba3dd3..d77322eb57 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -12581,7 +12581,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12604,7 +12604,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12627,7 +12627,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12650,7 +12650,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12673,7 +12673,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12696,7 +12696,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12719,7 +12719,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12742,7 +12742,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12765,7 +12765,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12788,7 +12788,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12811,7 +12811,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12834,7 +12834,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index c6a7a102c3..537b8f05a4 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19313,6 +19313,9 @@ "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -19700,6 +19703,9 @@ "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" ] }, @@ -19736,6 +19742,9 @@ "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" ] }, @@ -21734,6 +21743,9 @@ "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" ] }, @@ -21770,6 +21782,9 @@ "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" ] }, @@ -21806,6 +21821,9 @@ "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" ] }, @@ -21842,6 +21860,9 @@ "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" ] }, @@ -21878,6 +21899,9 @@ "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" ] }, @@ -21914,6 +21938,9 @@ "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" ] }, @@ -21950,6 +21977,9 @@ "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" ] }, @@ -21986,6 +22016,9 @@ "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" ] }, @@ -22022,6 +22055,9 @@ "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" ] }, @@ -22058,6 +22094,9 @@ "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" ] }, @@ -22094,6 +22133,9 @@ "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" ] }, @@ -22130,6 +22172,9 @@ "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" ] }, diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index 9bd3927b8c..cf77c1f842 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -594,7 +594,7 @@ function publicDetail(tier) { return "Matches Node's key formats, point encodings, private-key range checks, key-pair checks and errors on the supported curves; ECDH.convertKey is supported, and property reads and writes are refused."; } if (/^compiler-dedicated:(?:Cipheriv|Decipheriv|cipher\.|decipher\.)/.test(source)) { - return "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused."; + return "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'."; } if (source.startsWith("compiler-dedicated:")) return "Implemented by a dedicated static compiler/runtime path."; diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 23c7a159b6..0e608148ff 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -42,8 +42,8 @@ "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/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/fixtures/node-types/cipher.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.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"], "node-builtin.crypto": ["tests/corpus/1358-crypto-random.ts", "tests/corpus/1534-crypto-hash-chain.ts", "tests/corpus/2556-crypto-introspection.ts", "tests/corpus/2890-crypto-static-utility.ts", "tests/harness/npm-static.test.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts"], "node-builtin.dgram": ["tests/corpus/2597-dgram-send-ladders.cjs"], @@ -412,7 +412,7 @@ { "symbols": ["assert.partialDeepStrictEqual"], "status": "partial", "evidence": ["tests/corpus/5517-assert-partial-deep-equal.js", "tests/corpus/5518-assert-partial-deep-typed.ts", "tests/corpus/5519-assert-partial-deep-typed-edges.ts"] }, { "symbols": ["assert.deepEqual", "assert.notDeepEqual"], "status": "partial", "evidence": ["tests/corpus/5513-assert-deep-equal-loose.js", "tests/corpus/5514-assert-deep-equal-typed.ts", "tests/corpus/5516-assert-deep-equal-edges.js"] }, { "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] }, - { "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["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/fixtures/node-types/cipher.ts"] }, + { "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["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"] }, { "chapter": "crypto", "symbols": ["ECDH", "ecdh.computeSecret", "ecdh.generateKeys", "ecdh.getPrivateKey", "ecdh.getPublicKey", "ecdh.setPrivateKey", "ecdh.setPublicKey"], "status": "partial", "evidence": ["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"] }, { "chapter": "errors", "symbols": ["ERR_CRYPTO_ECDH_INVALID_FORMAT", "ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY"], "status": "partial", "evidence": ["tests/corpus/6702-ecdh-errors.js"] }, { "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] }, diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index f5ff2642e3..56ca468788 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: 24, marker: "scr_runtime_abi_v24" }, + runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, 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_v24"); + expect(error?.stderr).toContain("scr_runtime_abi_v25"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index dbb3cd8fda..16d7b98455 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -2852,7 +2852,8 @@ declare module "crypto" { ): void; export type BinaryLike = string | ArrayBuffer | ArrayBufferView; /* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, and gcm modes and - * chacha20-poly1305. The stream.Transform members are not declared. */ + * 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"; export type CipherChaCha20Poly1305Types = "chacha20-poly1305"; export type CipherKey = BinaryLike | KeyObject; @@ -2862,7 +2863,7 @@ declare module "crypto" { export interface CipherChaCha20Poly1305Options { authTagLength?: number; } - export interface Cipheriv { + export interface Cipheriv extends Transform { update(data: string | ArrayBufferView): Buffer; update(data: string, inputEncoding: BufferEncoding): Buffer; update(data: ArrayBufferView, inputEncoding: undefined, outputEncoding: BufferEncoding): string; @@ -2883,7 +2884,7 @@ declare module "crypto" { setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; getAuthTag(): Buffer; } - export interface Decipheriv { + export interface Decipheriv extends Transform { update(data: ArrayBufferView): Buffer; update(data: string, inputEncoding: BufferEncoding): Buffer; update(data: ArrayBufferView, inputEncoding: undefined, outputEncoding: BufferEncoding): string; diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8bb8bc3aab..524d5939d9 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 = { "procStream.write": "scr_proc_stream_write", "stream.stdioWritable": "scr_stream_stdio_writable", "stream.unboxDyn": "scr_stream_unbox_dyn", + "stream.handleStream": "scr_stream_handle", "stream.pipeDyn": "scr_stream_pipe_dyn", "stream.unpipeDyn": "scr_stream_unpipe_dyn", "process.kill": "scr_process_kill_named", diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index d3b4b1f5b6..5d198d306c 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -2,6 +2,7 @@ import { InternalCompilerError } from "../../errors.js"; import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js"; import { f64Lit } from "./common.js"; +import { pipelinePumpedRuns } from "../../ir/builtin-signatures.js"; const STREAM_CANONICAL_CBS: Record = { readable: ["r", "d"], @@ -662,14 +663,14 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV e.fn === "stream.pipelineStages" || e.fn === "stream.pipelineStagesDyn" ) { - // (n, at, k, flags, mask, s1..sn, stage1..stagek, (next, read, - // return)?, box?, (signal, abort)?[, cb]): the streams and the stage - // closures ride stack arrays; absent groups pass null. - const [nArg, atArg, kArg, flagsArg, maskArg] = e.args; + // (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. + const [nArg, kArg, positionsArg, flagsArg, maskArg] = e.args; if ( nArg?.kind !== "numLit" || - atArg?.kind !== "numLit" || kArg?.kind !== "numLit" || + positionsArg?.kind !== "numLit" || flagsArg?.kind !== "numLit" || maskArg?.kind !== "numLit" ) @@ -677,6 +678,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 args = e.args.map((a) => host.emitExpr(a)); const packed = (from: number, count: number): string => { if (count === 0) return "null"; @@ -691,20 +693,17 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV }; const streams = packed(5, n); const stages = packed(5 + n, k); - let at = 5 + n + k; + const helpers = packed(5 + n + k, helperCount); + let at = 5 + n + k + helperCount; const take = (present: boolean): string => (present ? args[at++]!.name : "null"); - const hasHelpers = (flags & 8) !== 0; - const next = take(hasHelpers); - const read = take(hasHelpers); - const ret = take(hasHelpers); - const box = take((flags & 16) !== 0); + const box = take(((flags >> 1) & 3) === 2); const signal = take((flags & 32) !== 0); const abort = take((flags & 32) !== 0); const head = - `double ${f64Lit(n)}, ptr ${streams}, double ${f64Lit(atArg.value)}, double ${f64Lit(k)}, ` + - `ptr ${stages}, double ${f64Lit(flags)}, double ${f64Lit(maskArg.value)}, ` + - `ptr ${next}, ptr ${read}, ptr ${ret}, ptr ${box}, ptr ${signal}, ptr ${abort}`; - const params = "double, ptr, double, double, ptr, double, double, ptr, ptr, ptr, ptr, ptr, ptr"; + `double ${f64Lit(n)}, ptr ${streams}, double ${f64Lit(k)}, ptr ${stages}, ` + + `double ${f64Lit(positionsArg.value)}, double ${f64Lit(flags)}, double ${f64Lit(maskArg.value)}, ` + + `ptr ${helpers}, ptr ${box}, ptr ${signal}, ptr ${abort}`; + const params = "double, ptr, double, ptr, double, double, double, ptr, ptr, ptr, ptr"; const t = B.tmp(); if (e.fn === "sp.pipelineStages") { host.declare(`declare ptr @scr_sp_pipeline_stages(${params})`); diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 5eb24482bb..f025103389 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 = 24 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v24" as const; +export const RUNTIME_ABI_VERSION = 25 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v25" 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 fc7ed9efa0..8bdbc84920 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: 24, marker: "scr_runtime_abi_v24" }, + runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, 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 5ffb4797de..a3ac9fe5d3 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -124,7 +124,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, + runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/frontend/lowering/binding-analysis.ts b/packages/compiler/src/frontend/lowering/binding-analysis.ts index 83b9aa6f92..1987c86baa 100644 --- a/packages/compiler/src/frontend/lowering/binding-analysis.ts +++ b/packages/compiler/src/frontend/lowering/binding-analysis.ts @@ -10,7 +10,13 @@ import { isGenericCallableMemberType } from "../type-mapper.js"; * Sound file-locally for module-scope bindings because ESM import * bindings are read-only — no other file can write one. Cached per * symbol (the scan walks the whole file once). */ -export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: ts.Node): boolean { +export function bindingNeverReassigned( + lowerer: Lowerer, + sym: ts.Symbol, + decl: ts.Node, + resolve: (ident: ts.Identifier) => ts.Symbol | null | undefined = (ident) => + lowerer.resolveValueSymbol(ident), +): boolean { const cached = lowerer.neverReassignedCache.get(sym); if (cached !== undefined) return cached; let written = false; @@ -18,7 +24,7 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t // identifiers pay a symbol resolution. const symText = ts.symbolName(sym); const namesSym = (e: ts.Node): boolean => - ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym; + ts.isIdentifier(e) && e.text === symText && resolve(e) === sym; const scanTarget = (t: ts.Expression): void => { let e: ts.Expression = t; while (ts.isParenthesizedExpression(e)) e = e.expression; diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index f4270dbe82..3dd3a42781 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -907,7 +907,8 @@ export function v8CalleeText(node: ts.Expression): string { /** Calls on native Cipheriv/Decipheriv handles dispatch through the * handle's methods with checked-dynamic arguments. The stream.Transform - * members the classes inherit are refused. */ + * members the classes inherit lower over the cipher's Transform + * (lowerHashStreamCall, which runs first); other members are refused. */ export function lowerCipherMethodCall( lowerer: Lowerer, call: ts.CallExpression, @@ -920,7 +921,7 @@ export function lowerCipherMethodCall( lowerer.noLowering( `Cipheriv/Decipheriv.${name}`, call, - "update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not", + "update(), final(), setAAD(), setAutoPadding(), getAuthTag(), setAuthTag(), and the stream members are supported", ); } if (call.arguments.nodes.some(ts.isSpreadElement)) diff --git a/packages/compiler/src/frontend/lowering/builtins/hash-stream.ts b/packages/compiler/src/frontend/lowering/builtins/hash-stream.ts index 382d15eaf1..f2f3625dd5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/hash-stream.ts +++ b/packages/compiler/src/frontend/lowering/builtins/hash-stream.ts @@ -1,17 +1,27 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; +import type { SrcLoc } from "../../../ir/ir.js"; import { varRef } from "../../../ir/build.js"; -import { type IrExpr, type IrType } from "../../../ir/ir.js"; +import { type IrExpr, type IrStmt, type IrType } from "../../../ir/ir.js"; +import { withStreamReceiver } from "../dyn-streams.js"; import { EMITTER_API_MEMBERS } from "../lower-classes.js"; import { lowerEmitterMethodCall } from "../lower-event-emitter.js"; -import { STREAM_API_MEMBERS, STREAM_OPERATORS, lowerStreamMethodCall } from "../lower-stream.js"; +import { + STREAM_API_MEMBERS, + STREAM_OPERATORS, + lowerStreamMethodCall, + lowerStreamProperty, +} from "../lower-stream.js"; +import { isCipherType } from "./crypto.js"; -/* node:crypto's Hash and Hmac are Transforms: a value written, read, - * listened to, or piped as a stream reads and writes through its own - * native Transform (scr_stream.c), created on first use. update() and - * digest() keep the value's static lowering; the stream members lower - * through the stream and emitter spokes over that Transform. */ +/* node:crypto's Hash, Hmac, Cipheriv and Decipheriv are Transforms: a + * value written, read, listened to, or piped as a stream reads and writes + * through its own native Transform, which scr_stream.c creates on first + * use. update(), digest() and final() keep the value's own lowering; the + * stream members lower through the stream and emitter spokes over that + * Transform. A Hash or Hmac is known by its IR type; a cipher is a + * checked-dynamic handle and is known by its checker type. */ const TRANSFORM_CLASS = "%Transform"; const TRANSFORM: IrType = { kind: "object", className: TRANSFORM_CLASS }; @@ -20,6 +30,12 @@ export function isHashValueType(type: IrType | null | undefined): boolean { return type?.kind === "cryptoHash" || type?.kind === "cryptoHmac"; } +/** Whether `node` evaluates to a Hash, Hmac, Cipheriv or Decipheriv. */ +export function isValueStream(lowerer: Lowerer, node: ts.Expression): boolean { + const type = lowerer.typeOf(node); + return isHashValueType(lowerer.mapTypeOf(type)) || isCipherType(lowerer, type); +} + /** The Transform of a lowered Hash or Hmac value (+1). A pipe * destination or pipeline stage (`piped`) also keeps the value alive * until its stream flushes or is destroyed. */ @@ -33,9 +49,27 @@ export function hashStreamOf(value: IrExpr, piped = false): IrExpr { }; } -/** A stream member called on a Hash or Hmac (`hash.write(chunk)`, - * `hash.on("data", cb)`, `hash.setEncoding("hex")`). Members that answer - * the stream (`this`) answer the Hash itself. */ +/** The Transform of a lowered Hash, Hmac, Cipheriv or Decipheriv value + * (+1), with hashStreamOf's `piped` (a cipher's stream always holds its + * cipher). */ +export function valueStreamOf(value: IrExpr, piped = false): IrExpr { + if (isHashValueType(value.type)) return hashStreamOf(value, piped); + // A value already lowered as its stream (addAbortSignal answers the + // Transform it was given, which its generic signature types as the + // cipher). + if (value.type.kind === "object") return value; + return { + kind: "libCall", + fn: "stream.handleStream", + args: [value], + type: TRANSFORM, + loc: value.loc, + }; +} + +/** A stream member called on a Hash, Hmac, Cipheriv or Decipheriv + * (`hash.write(chunk)`, `cipher.on("data", cb)`, `hash.setEncoding("hex")`). + * Members that answer the stream (`this`) answer the value itself. */ export function lowerHashStreamCall( lowerer: Lowerer, call: ts.CallExpression, @@ -45,21 +79,48 @@ export function lowerHashStreamCall( const name = access.name.text; const streamMember = STREAM_API_MEMBERS.has(name) || STREAM_OPERATORS.has(name); if (!streamMember && !EMITTER_API_MEMBERS.has(name)) return null; - if (!isHashValueType(lowerer.mapTypeOf(lowerer.typeOf(access.expression)))) return null; + if (!isValueStream(lowerer, access.expression)) return null; const info = lowerer.classes.get(TRANSFORM_CLASS); if (!info) return null; - const loc = locOf(call); - const hash = lowerer.lowerExpr(access.expression); - const local = lowerer.declareHiddenLocal("%hash", hash.type); - // The spokes lower the receiver to the value's Transform. - lowerer.hashStreamReceivers.set(access.expression, { localId: local.id, type: hash.type }); + return withValueStreamReceiver(lowerer, access.expression, locOf(call), () => + streamMember + ? lowerStreamMethodCall(lowerer, call, access, info) + : lowerEmitterMethodCall(lowerer, call, access, info), + ); +} + +/** A stream property read on a Hash, Hmac, Cipheriv or Decipheriv + * (`cipher.destroyed`, `hash.readableEnded`). */ +export function lowerValueStreamProperty( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): IrExpr | null { + if (expr.questionDotToken || !isValueStream(lowerer, expr.expression)) return null; + const info = lowerer.classes.get(TRANSFORM_CLASS); + if (!info) return null; + return withValueStreamReceiver(lowerer, expr.expression, locOf(expr), () => + lowerStreamProperty(lowerer, expr, info), + ); +} + +/** Evaluates the value `receiver` names once into a hidden local and runs + * `lower` with the receiver lowering to that value's Transform. A result + * typed as the Transform (a member that answers `this`) answers the value + * itself. */ +function withValueStreamReceiver( + lowerer: Lowerer, + receiver: ts.Expression, + loc: SrcLoc, + lower: () => IrExpr | null, +): IrExpr | null { + const value = lowerer.lowerExpr(receiver); + const local = lowerer.declareHiddenLocal("%streamValue", value.type); + lowerer.valueStreamReceivers.set(receiver, { localId: local.id, type: value.type }); let lowered: IrExpr | null; try { - lowered = streamMember - ? lowerStreamMethodCall(lowerer, call, access, info) - : lowerEmitterMethodCall(lowerer, call, access, info); + lowered = lower(); } finally { - lowerer.hashStreamReceivers.delete(access.expression); + lowerer.valueStreamReceivers.delete(receiver); } if (!lowered) return null; const answersStream = @@ -68,16 +129,42 @@ export function lowerHashStreamCall( ? { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: lowered, loc }], - result: varRef(local.id, hash.type, loc), - type: hash.type, + result: varRef(local.id, value.type, loc), + type: value.type, loc, } : lowered; return { kind: "seqExpr", - stmts: [{ kind: "varDecl", localId: local.id, init: hash, loc }], + stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }], result, type: result.type, loc, }; } + +/** `for await (const chunk of value)` over a Hash, Hmac, Cipheriv or + * Decipheriv: the loop iterates the value's Transform. */ +export function lowerForAwaitValueStream( + lowerer: Lowerer, + stmt: ts.ForOfStatement, + lowerLoop: () => IrStmt | null, +): IrStmt | null { + if (!isValueStream(lowerer, stmt.expression)) return null; + const loc = locOf(stmt.expression); + const local = lowerer.declareHiddenLocal("%streamIterated", TRANSFORM); + const init = valueStreamOf(lowerer.lowerExpr(stmt.expression)); + const loop = withStreamReceiver( + lowerer, + stmt.expression, + varRef(local.id, TRANSFORM, loc), + TRANSFORM, + lowerLoop, + ); + if (loop === null) return null; + return { + kind: "block", + body: [{ kind: "varDecl", localId: local.id, init, loc }, loop], + loc: locOf(stmt), + }; +} diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 4b20d7bf55..32fb03644e 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1078,11 +1078,20 @@ export function lowerBytesMethodCall( // Array-toString (comma join), a different operation: fenced. const declaredOnBuffer = (() => { const nameSym = lowerer.checker.getSymbolAtLocation(access.name); - return ( + if ( nameSym !== undefined && nameSym.declarations.some( (d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer", ) + ) + return true; + // A receiver the lowering types as a Buffer where the checker says + // `any` (a JavaScript pipeline stage's chunk). + const receiverSym = lowerer.typeOf(access.expression).getSymbol(); + return ( + receiverSym !== undefined && + ts.symbolName(receiverSym) === "Buffer" && + receiverSym.declarations.some((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) ); })(); if (receiverIr.elem !== "u8" || !declaredOnBuffer) { diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index 48b4caabdb..ba5d0684da 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -1223,7 +1223,7 @@ class HostFrame implements ChainFrame { /** The locals and parameters of a lifted helper function under * construction. */ -class LiftedFrame implements ChainFrame { +export class LiftedFrame implements ChainFrame { readonly locals: IrLocal[] = []; readonly params: IrParam[] = []; private n = 0; @@ -1253,7 +1253,7 @@ class LiftedFrame implements ChainFrame { * step leave it unclosed, as in Node. The generator counts as unfinished * before the first step, so a `pre` that breaks at once (take(0)) closes * a source it never pulled. `genT` must be stepDrivable. */ -function driveGenerator( +export function driveGenerator( lowerer: Lowerer, frame: ChainFrame, genId: string, diff --git a/packages/compiler/src/frontend/lowering/dyn-streams.ts b/packages/compiler/src/frontend/lowering/dyn-streams.ts index 2ac09e8e59..2167eba238 100644 --- a/packages/compiler/src/frontend/lowering/dyn-streams.ts +++ b/packages/compiler/src/frontend/lowering/dyn-streams.ts @@ -76,7 +76,7 @@ function dynNativeStream( /** Runs `lower` with `node` lowering to `value` and its member calls taking * `type` as the receiver representation. */ -function withStreamReceiver( +export function withStreamReceiver( lowerer: Lowerer, node: ts.Expression, value: IrExpr, diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 6c8dfa3bf0..4d8e4d52c9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -132,6 +132,7 @@ import { lowerProtocolPropertyRead } from "../class-protocol-dispatch.js"; import { lowerReadlineProperty } from "../builtins/readline.js"; import { lowerEventStaticProperty } from "../builtins/numbers.js"; import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "../lower-stream.js"; +import { lowerValueStreamProperty } from "../builtins/hash-stream.js"; import { lowerEventObjectProperty } from "../lower-event-target.js"; import { iteratorResultValueRead } from "../lower-generators.js"; import { lowerGlobalValue } from "../lower-global-value.js"; @@ -3995,6 +3996,8 @@ function lowerStreamObjectProperty( expr: ts.PropertyAccessExpression, ): IrExpr | null { if (expr.questionDotToken && !lowerer.chainHandled.has(expr)) return null; + const valueStream = lowerValueStreamProperty(lowerer, expr); + if (valueStream) return valueStream; const recvT = lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); if (recvT?.kind !== "object") return null; const info = lowerer.classes.get(recvT.className); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index e3480cf9c8..bc3d2e8699 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -3,6 +3,7 @@ import { declaredMemberOf, isPromisePrototypeRead } from "./expressions/property import { hostsAll } from "./expressions/hosted-narrowing.js"; import { requestGeneralStreamSurface } from "./general-streams.js"; import { lowerForAwaitDynStream } from "./dyn-streams.js"; +import { lowerForAwaitValueStream } from "./builtins/hash-stream.js"; import { lowerChildPropertyWrite } from "./handle-properties.js"; import { matchAllRowsReadByElements, @@ -9390,10 +9391,11 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { ) { return lowerForAwaitStdin(lowerer, stmt); } - // A checked-dynamic NodeJS.ReadableStream iterates its native stream. - const dynStream = lowerForAwaitDynStream(lowerer, stmt, () => - lowerForAwaitBuiltin(lowerer, stmt), - ); + // A checked-dynamic NodeJS.ReadableStream iterates its native stream, + // and a Hash, Hmac, Cipheriv or Decipheriv iterates its Transform. + const dynStream = + lowerForAwaitDynStream(lowerer, stmt, () => lowerForAwaitBuiltin(lowerer, stmt)) ?? + lowerForAwaitValueStream(lowerer, stmt, () => lowerForAwaitBuiltin(lowerer, stmt)); if (dynStream) return dynStream; const builtin = lowerForAwaitBuiltin(lowerer, stmt); if (builtin) return builtin; diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index a75ec43b72..ab21b8a7e2 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -1,6 +1,6 @@ import { keyedHelperName } from "./stable-names.js"; import { nodeThrowExpr, boolLit, numLit, strLit } from "../../ir/build.js"; -import { hashStreamOf, isHashValueType } from "./builtins/hash-stream.js"; +import { isValueStream, valueStreamOf } from "./builtins/hash-stream.js"; import { InternalCompilerError } from "../../errors.js"; /* The node:stream lowering (the spoke-module pattern, like * lower-event-emitter.ts): construction of the five runtime stream classes @@ -52,7 +52,10 @@ import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { genResultRecord } from "../type-mapper.js"; import { noteUntypedStreamFlow, requestGeneralStreamSurface } from "./general-streams.js"; import { dynNativeBranch } from "./dyn-streams.js"; +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 { BOOL, DYN, @@ -74,6 +77,7 @@ import { funcOf, typeEquals, typeKey, + UNDEFINED_T, } from "../../ir/ir.js"; const BYTES = BYTES_U8; @@ -2084,10 +2088,10 @@ function lowerStreamArg( what: "finished" | "pipeline" | "addAbortSignal" | "compose", ): IrExpr { const lowered = lowerer.lowerExpr(node); - // A Hash or Hmac stage is its Transform; pipeline and compose stages - // keep the value alive until the stream settles. - const v = isHashValueType(lowered.type) - ? hashStreamOf(lowered, what === "pipeline" || what === "compose") + // A Hash, Hmac, Cipheriv or Decipheriv stage is its Transform; pipeline + // and compose stages keep the value alive until the stream settles. + const v = isValueStream(lowerer, node) + ? valueStreamOf(lowered, what === "pipeline" || what === "compose") : lowered; // A checked-dynamic pipeline stage (an untyped source or destination): // a boxed native stream unwraps at run time; other values (iterables, @@ -2429,10 +2433,10 @@ export function lowerStreamModuleCall( ? pipelineOptions(lowerer, lastArg) : null; const stageArgs = opts ? args.slice(0, -1) : args; - if (stageArgs.some(isPipelineFunctionStage)) { + if (hasPipelineFunctionStage(lowerer, stageArgs)) { if (opts) { lowerer.noLowering( - "pipeline with function stages and options", + "pipeline with function stages or iterable sources and options", lastArg, "pipeline(source, async function* (source) { ... }, destination) without options is supported", ); @@ -2503,7 +2507,10 @@ export function lowerStreamModuleCall( // (finished/pipeline throw synchronously; consumers do not). A // sync throw here would be observably wrong timing, so the // provable case takes the named fence instead. - const argV = lowerer.lowerExpr(args[0]!); + // A Hash, Hmac, Cipheriv or Decipheriv is read through its Transform. + const argV = isValueStream(lowerer, args[0]!) + ? valueStreamOf(lowerer.lowerExpr(args[0]!)) + : lowerer.lowerExpr(args[0]!); const argInfo = argV.type.kind === "object" ? lowerer.classes.get(argV.type.className) : undefined; if (argV.type.kind !== "dyn" && !streamSidesOf(lowerer, argInfo)) { @@ -2621,7 +2628,7 @@ export function lowerStreamModuleCall( "the supported form is pipeline(source, ...streams, callback)", ); } - if (args.slice(0, -1).some(isPipelineFunctionStage)) { + 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 @@ -2680,19 +2687,215 @@ export function lowerStreamModuleCall( return null; } -/** A pipeline() argument written as a function: an async generator - * function stage. */ -function isPipelineFunctionStage(node: ts.Expression): boolean { - return ts.isFunctionExpression(node) || ts.isArrowFunction(node); +/** A pipeline() function stage: an inline function or arrow function, or + * an identifier bound to a function declaration or to a `const` + * initialized with one, which nothing reassigns. */ +type StageFunction = ts.FunctionExpression | ts.ArrowFunction | ts.FunctionDeclaration; + +/** The function a pipeline() argument writes or names, or null when the + * argument is not a function stage. */ +function pipelineStageFunction(lowerer: Lowerer, node: ts.Expression): StageFunction | null { + if (ts.isFunctionExpression(node) || ts.isArrowFunction(node)) return node; + if (!ts.isIdentifier(node)) return null; + // The stage lowers the declaration's body again with its source typed + // by the pipeline, so resolving it here must not report what collecting + // the declaration's own signature deferred (resolveValueSymbol would). + const resolve = (ident: ts.Identifier): ts.Symbol | undefined => { + const found = lowerer.checker.getSymbolAtLocation(ident); + return found && found.flags & ts.SymbolFlags.Alias + ? lowerer.checker.getAliasedSymbol(found) + : found; + }; + const symbol = resolve(node); + const declarations = symbol?.declarations ?? []; + const bodies = declarations.filter((d) => !ts.isFunctionDeclaration(d) || d.body !== undefined); + if (!symbol || bodies.length !== 1) return null; + const decl = bodies[0]!; + if (lowerer.isStdlibFile(ts.getSourceFileOfNode(decl))) return null; + if (ts.isFunctionDeclaration(decl)) { + return bindingNeverReassigned(lowerer, symbol, decl, resolve) ? decl : null; + } + if ( + ts.isVariableDeclaration(decl) && + decl.initializer && + ts.isVariableDeclarationList(decl.parent) && + (decl.parent.flags & ts.NodeFlags.Const) !== 0 + ) { + let init: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) return init; + } + return null; +} + +function hasPipelineFunctionStage(lowerer: Lowerer, args: readonly ts.Expression[]): boolean { + return ( + args.some((a) => pipelineStageFunction(lowerer, a) !== null) || + (args.length > 0 && iterableSourceType(lowerer, args[0]!) !== null) + ); +} + +type SyncGenType = IrType & { kind: "generator" }; + +/** The element type of a pipeline source that is an iterable value (an + * array, a string or a synchronous generator object), or null. */ +function iterableSourceType(lowerer: Lowerer, node: ts.Expression): IrType | null { + if (pipelineStageFunction(lowerer, node) !== null) return null; + let t: IrType | null; + try { + t = lowerer.mapTypeOf(lowerer.typeOf(node)); + } catch { + return null; + } + if (t?.kind === "array") return t.elem; + if (t?.kind === "string") return STRING; + if (t?.kind === "generator" && t.async !== true && stepDrivable(t)) return t.yieldT; + return null; } -/** The lowered function stages of a pipeline: the libcall head [n, at, k, - * flags, mask, streams..., stages..., (next, read, return)?, box?, +/** A pipeline source that is an iterable value or a synchronous generator + * function without parameters, as an async generator closure that steps + * the value's iterator (or the generator the function returns) and yields + * each element. Node's pipeline iterates such a source with `for await`, + * through an async-from-sync iterator, and an async generator that + * yields the same elements resumes its consumer on the same ticks. Null + * when the source has no such lowering. */ +function lowerIterableSource( + lowerer: Lowerer, + node: ts.Expression, + fn: StageFunction | null, + loc: SrcLoc, +): { closure: IrExpr; out: SyncGenType } | null { + let value: IrExpr; + if (fn !== null) { + if (fn.parameters.nodes.length > 0) { + lowerer.noLowering( + "a synchronous generator function pipeline source with parameters", + fn.parameters.nodes[0]!, + "declare the source without parameters, or make it an async generator function", + ); + } + value = lowerer.lowerLambda(fn); + if (value.type.kind !== "func" || value.type.ret.kind !== "generator") return null; + } else { + value = lowerer.lowerExpr(node); + } + const srcT = value.type; + const elemT: IrType | null = + srcT.kind === "func" && srcT.ret.kind === "generator" + ? srcT.ret.yieldT + : srcT.kind === "array" + ? srcT.elem + : srcT.kind === "string" + ? STRING + : srcT.kind === "generator" && srcT.async !== true + ? srcT.yieldT + : null; + if (elemT === null) return null; + const out: SyncGenType = { + kind: "generator", + async: true, + yieldT: elemT, + retT: VOID, + nextT: UNDEFINED_T, + }; + const resultType = genResultRecord(elemT, VOID, lowerer.shapes, lowerer.unions); + if (!resultType) return null; + const key = `pipelineSource:${typeKey(srcT)}`; + let maker = lowerer.valueHelpers.get(key); + if (!maker) { + maker = `%stream.pipelineSource.${lowerer.valueHelpers.size}`; + const genName = `${maker}.gen`; + // The generator: g = the source's iterator; for (v of g) yield v. + const frame = new LiftedFrame(loc); + const srcId = frame.local("src", srcT, false); + const src = varRef(srcId, srcT, loc); + const syncT: SyncGenType = { + kind: "generator", + yieldT: elemT, + retT: VOID, + nextT: UNDEFINED_T, + }; + let iterator: IrExpr | null; + if (srcT.kind === "func") { + iterator = { kind: "callValue", callee: src, args: [], type: srcT.ret, loc }; + } else if (srcT.kind === "array") { + iterator = lowerArrayIteratorValue(lowerer, src, "values", syncT, loc); + } else if (srcT.kind === "string") { + iterator = lowerStringIteratorValue(lowerer, src, syncT, loc); + } else { + iterator = src; + } + if (iterator === null || iterator.type.kind !== "generator" || !stepDrivable(iterator.type)) + return null; + const iterT = iterator.type; + const gId = frame.local("g", iterT, false); + const body: IrStmt[] = [ + { kind: "varDecl", localId: gId, init: iterator, loc }, + ...driveGenerator( + lowerer, + frame, + gId, + iterT, + elemT, + [], + (v) => [{ kind: "exprStmt", expr: { kind: "yieldExpr", value: v, type: VOID, loc }, loc }], + loc, + ), + { kind: "return", value: null, loc }, + ]; + const srcLocal = frame.locals.find((l) => l.id === srcId)!; + srcLocal.boxed = true; + lowerer.valueHelpers.set(key, maker); + lowerer.liftedFns.push({ + name: genName, + params: [], + returnType: VOID, + async: true, + captures: [{ localId: srcId, name: "src", type: srcT }], + locals: frame.locals, + body, + generator: { yieldT: elemT, nextT: UNDEFINED_T, resultType }, + loc, + }); + // The maker: (src) => the generator function closed over src. + const makerSrc: IrLocal = { id: "src.0", name: "src", type: srcT, mutable: false, boxed: true }; + lowerer.liftedFns.push({ + name: maker, + params: [{ localId: makerSrc.id, name: makerSrc.name, type: srcT }], + returnType: funcOf([], out), + locals: [makerSrc], + body: [ + { + kind: "return", + value: { + kind: "closure", + fnName: genName, + captures: [makerSrc.id], + type: funcOf([], out), + loc, + }, + loc, + }, + ], + loc, + }); + } + return { + closure: { kind: "call", callee: maker, args: [value], type: funcOf([], out), loc }, + out, + }; +} + +/** The lowered function stages of a pipeline: the libcall head [n, k, + * positions, flags, mask, streams..., stages..., helpers..., box?, * (signal, abort)?], the destination's type (the last stream, or the * PassThrough a function destination answers), the destination kind * (SCR_PIPE_DEST_*), a promise destination's promise type, and a wrapper * that binds the pipeline's AbortController around the call when a - * stage takes `{ signal }`. */ + * stage takes `{ signal }`. positions has bit j set when the pipeline's + * j-th stage is a function; helpers holds (next, read, return) for each + * generator pumped into a stream. */ interface PipelineStagesLowering { args: IrExpr[]; last: IrType; @@ -2701,56 +2904,89 @@ interface PipelineStagesLowering { wrap: (call: IrExpr) => IrExpr; } -/** pipeline(...) with one run of function stages (pipeline.js). The run - * may start the pipeline (a source function, called with `{ signal }`) - * and may end it (a destination function: an async generator pumped into - * a PassThrough, or an async function whose promise's value reaches the - * callback or the promise). 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 +/** 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 + * pipeline passes it: makeAsyncIterable answers the previous stream + * itself (a Readable is async iterable) or the previous stage's * generator. */ function lowerPipelineStages( lowerer: Lowerer, stageArgs: readonly ts.Expression[], loc: SrcLoc, ): PipelineStagesLowering { - const first = stageArgs.findIndex(isPipelineFunctionStage); - let end = first; - while (end < stageArgs.length && isPipelineFunctionStage(stageArgs[end]!)) end++; - if (stageArgs.slice(end).some(isPipelineFunctionStage)) { + if (stageArgs.length > 52) { lowerer.noLowering( - "pipeline with function stages separated by streams", - stageArgs[end]!, - "one run of function stages is supported", + "pipeline with function stages and more than 52 stages", + stageArgs[52]!, + "split the pipeline", ); } - const source = first === 0; - const destFn = end === stageArgs.length; - const before: IrExpr[] = []; - const after: IrExpr[] = []; - try { - for (const a of stageArgs.slice(0, first)) before.push(lowerStreamArg(lowerer, a, "pipeline")); - } catch (e) { - if (e instanceof StreamArgTypeThrow) - lowerer.noLowering("pipeline over this value", stageArgs[0]!); - throw e; - } - let srcT: IrType | null = source ? null : before[before.length - 1]!.type; + const streams: IrExpr[] = []; const stages: IrExpr[] = []; + const helperArgs: IrExpr[] = []; + let positions = 0; let mask = 0; let dest: 0 | 2 = 0; let destPromise: (IrType & { kind: "promise" }) | null = null; - for (let i = first; i < end; i++) { - const fn = stageArgs[i] as ts.FunctionExpression | ts.ArrowFunction; - const isLast = destFn && i === end - 1; + // What the next function stage receives: null before the first stage, + // else the previous stream's type or the previous stage's generator. + let srcT: IrType | null = null; + for (let j = 0; j < stageArgs.length; j++) { + const arg = stageArgs[j]!; + const fn = 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))) { + const source = lowerIterableSource(lowerer, arg, syncSourceFn ? fn : null, loc); + if (source === null) lowerer.noLowering("pipeline over this source", arg); + positions += 1; + stages.push(source!.closure); + srcT = source!.out; + continue; + } + if (fn === null) { + if (srcT?.kind === "generator") { + // the generator before this stream, pumped into it + if (!fromGenChunk(lowerer, srcT.yieldT)) { + lowerer.noLowering( + `a pipeline stage yielding '${lowerer.fmt(srcT.yieldT)}'`, + stageArgs[j - 1]!, + "stage values written to a stream are strings or Buffers", + ); + } + const helpers = fromGenHelpers(lowerer, srcT, loc); + if (!helpers) + lowerer.noLowering("a pipeline stage with this generator type", stageArgs[j - 1]!); + helperArgs.push(helpers![0]!, helpers![1]!, helpers![2]!); + } + let stream: IrExpr; + try { + stream = lowerStreamArg(lowerer, arg, "pipeline"); + } catch (e) { + if (e instanceof StreamArgTypeThrow) lowerer.noLowering("pipeline over this value", arg); + throw e; + } + streams.push(stream); + srcT = stream.type; + continue; + } + const isLast = j === stageArgs.length - 1; const isAsync = (fn.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); - const isGen = ts.isFunctionExpression(fn) && fn.asteriskToken !== undefined; + 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", - fn, + arg, "end the pipeline with a stream or an async function (source) { for await (const chunk of source) ... }", ); } @@ -2761,7 +2997,7 @@ function lowerPipelineStages( : srcT === null ? "a pipeline source function that is not an async generator function" : "pipeline stages that are not async generator functions", - fn, + arg, "write the stage as async function* (source) { for await (const chunk of source) ... }", ); } @@ -2783,8 +3019,9 @@ function lowerPipelineStages( "type it as { signal: AbortSignal }", ); } - mask |= 1 << (i - first); + mask |= 1 << stages.length; } + positions += 2 ** j; stages.push(lowered.closure); if (lowered.out.kind === "promise") { dest = 2; @@ -2794,29 +3031,7 @@ function lowerPipelineStages( srcT = lowered.out; } } - try { - for (const a of stageArgs.slice(end)) after.push(lowerStreamArg(lowerer, a, "pipeline")); - } catch (e) { - if (e instanceof StreamArgTypeThrow) - lowerer.noLowering("pipeline over this value", stageArgs[end]!); - throw e; - } - const helperArgs: IrExpr[] = []; - if (dest !== 2) { - // the generator pumped into the next stream or the PassThrough - const lastGen = srcT as IrType & { kind: "generator" }; - if (!fromGenChunk(lowerer, lastGen.yieldT)) { - lowerer.noLowering( - `a pipeline stage yielding '${lowerer.fmt(lastGen.yieldT)}'`, - stageArgs[end - 1]!, - "stage values written to a stream are strings or Buffers", - ); - } - const helpers = fromGenHelpers(lowerer, lastGen, loc); - if (!helpers) - lowerer.noLowering("a pipeline stage with this generator type", stageArgs[end - 1]!); - helperArgs.push(helpers![0]!, helpers![1]!, helpers![2]!); - } else { + if (dest === 2) { const valueT = destPromise!.inner; if ( valueT.kind !== "void" && @@ -2828,28 +3043,25 @@ function lowerPipelineStages( ) { lowerer.noLowering( `a pipeline destination resolving to '${lowerer.fmt(valueT)}'`, - stageArgs[end - 1]!, + stageArgs[stageArgs.length - 1]!, ); } helperArgs.push(pipelineBoxHelper(lowerer, destPromise!, loc)); } - const streams = [...before, ...after]; - // SCR_PIPE_* flags: bit 0 a source function, bits 1-2 the destination, - // bit 3 the helpers, bit 4 the box, bit 5 the signal and abort. - const flags = (source ? 1 : 0) | (dest << 1) | (dest !== 2 ? 8 : 16) | (mask !== 0 ? 32 : 0); + // SCR_PIPE_* flags: bits 1-2 the destination, bit 5 the signal and abort. + const flags = (dest << 1) | (mask !== 0 ? 32 : 0); const head: IrExpr[] = [ numLit(streams.length, loc), - numLit(before.length, loc), numLit(stages.length, loc), + numLit(positions, loc), numLit(flags, loc), numLit(mask, loc), ...streams, ...stages, ...helperArgs, ]; - const last: IrType = destFn - ? { kind: "object", className: "%PassThrough" } - : streams[streams.length - 1]!.type; + const last: IrType = + dest === 2 ? { kind: "object", className: "%PassThrough" } : streams[streams.length - 1]!.type; if (mask === 0) return { args: head, last, dest, destPromise, wrap: (call) => call }; // const ac = new AbortController(): its signal goes to the stages, and // finishImpl's ac.abort() runs through the abort closure. @@ -2980,7 +3192,7 @@ function pipelineAbortHelper(lowerer: Lowerer, ac: IrExpr, loc: SrcLoc): IrExpr * generator, or a promise for a destination async function. */ function lowerStageFunction( lowerer: Lowerer, - fn: ts.FunctionExpression | ts.ArrowFunction, + fn: StageFunction, srcT: IrType | null, ): { closure: IrExpr; out: IrType & ({ kind: "generator" } | { kind: "promise" }) } { const param = srcT === null ? undefined : fn.parameters.nodes[0]; @@ -2995,6 +3207,8 @@ function lowerStageFunction( "name the source parameter", ); } + // The loop variables of the `for await` loops over the source. + const chunkSymbols: ts.Symbol[] = []; if (symbol && fn.body) { const visit = (node: ts.Node): void => { if ( @@ -3017,14 +3231,36 @@ function lowerStageFunction( "iterate the source with for await (const chunk of source)", ); } + const binding = ts.isVariableDeclarationList(parent.initializer) + ? parent.initializer.declarations.nodes[0]?.name + : undefined; + const chunk = + binding && ts.isIdentifier(binding) + ? lowerer.checker.getSymbolAtLocation(binding) + : undefined; + if (chunk) chunkSymbols.push(chunk); } ts.forEachChild(node, visit); }; visit(fn.body); } if (symbol) lowerer.stageSourceTypes.set(symbol, srcT!); + // In JavaScript the checker types the chunks of a source that is the + // previous stage's generator `any`; they read as the strings or Buffers + // the generator yields, so their members take the string and Buffer + // lowerings. + const previousImplicit = lowerer.implicitParamTypes; + const chunkType = + srcT?.kind === "generator" && isJsSourceFile(ts.getSourceFileOfNode(fn)) + ? stageChunkCheckerType(lowerer, srcT.yieldT, fn) + : null; + if (chunkType && chunkSymbols.length > 0) { + const implicit = new Map(previousImplicit ?? []); + for (const chunk of chunkSymbols) implicit.set(chunk, chunkType); + lowerer.implicitParamTypes = implicit; + } try { - const closure = lowerer.lowerExpr(fn); + const closure = lowerer.lowerLambda(fn); const out = closure.type.kind === "func" ? closure.type.ret : null; if (!(out?.kind === "generator" && out.async) && out?.kind !== "promise") { lowerer.noLowering("a pipeline stage that does not return an async generator", fn); @@ -3032,9 +3268,19 @@ function lowerStageFunction( return { closure, out: out as IrType & ({ kind: "generator" } | { kind: "promise" }) }; } finally { if (symbol) lowerer.stageSourceTypes.delete(symbol); + lowerer.implicitParamTypes = previousImplicit; } } +/** 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 { + if (chunk.kind === "string") return lowerer.checker.getStringType(); + if (chunk.kind !== "bytes") return null; + const buffer = lowerer.checker.resolveName("Buffer", ts.SymbolFlags.Type, at, false); + return buffer ? lowerer.checker.getDeclaredTypeOfSymbol(buffer) : null; +} + /** `stream.compose(...streams)` over stream instances: a Duplex writing * to the first stream and reading from the last (scr_stream_compose). The * classes fix which positions can be readable and writable; a stream in @@ -3806,9 +4052,10 @@ export function lowerStreamMethodCall( "the supported forms are (destination) and (destination, { end })", ); } - // A Hash or Hmac destination pipes into its Transform, and pipe() - // answers the Hash itself (the receiver evaluates first, as in JS). - if (isHashValueType(lowerer.mapTypeOf(lowerer.typeOf(args[0]!)))) { + // A Hash, Hmac, Cipheriv or Decipheriv destination pipes into its + // 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 source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type); const hashValue = lowerer.lowerExpr(args[0]!); @@ -3826,7 +4073,10 @@ export function lowerStreamMethodCall( fn: "readable.pipe", args: [ { kind: "varRef", localId: source.id, type: receiverValue.type, loc }, - hashStreamOf({ kind: "varRef", localId: hash.id, type: hashValue.type, loc }, true), + valueStreamOf( + { kind: "varRef", localId: hash.id, type: hashValue.type, loc }, + true, + ), end, ], type: { kind: "object", className: "%Transform" }, diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3f8a0a66e5..f554985998 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -62,7 +62,7 @@ import { resolveRequireRuntime } from "../runtime-resolve.js"; import { lowerKeyObjectCall } from "./builtins/key-object.js"; import { isEcdhType, lowerEcdhCall } from "./builtins/ecdh.js"; import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js"; -import { hashStreamOf } from "./builtins/hash-stream.js"; +import { valueStreamOf } from "./builtins/hash-stream.js"; import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js"; import { iteratorSlotAdapter } from "./iterator-adapters.js"; @@ -3030,10 +3030,10 @@ export class Lowerer { /** Inline Promise.then parameters whose contextual type is a builtin * module namespace anonymous object. The settled native token is the * ABI truth even when the checker type has no stable module symbol. */ - /** Receivers of a stream call on a Hash or Hmac (lowerHashStreamCall): - * for the duration of that call, the receiver lowers to the Transform of - * the value held in the named local. */ - readonly hashStreamReceivers = new Map(); + /** Receivers of a stream call on a Hash, Hmac, Cipheriv or Decipheriv + * (lowerHashStreamCall): for the duration of that call, the receiver + * lowers to the Transform of the value held in the named local. */ + readonly valueStreamReceivers = new Map(); /** fs call arguments evaluated ahead of a URL path's conversion * (builtins/fs-paths.ts): each lowers to the hidden local that holds its * value, which is null until the path's lowering has saved it. */ @@ -12486,8 +12486,9 @@ export class Lowerer { /* ── expressions ──────────────────────────────────────────────────── */ lowerExpr(expr: ts.Expression): IrExpr { - const hash = this.hashStreamReceivers.get(expr); - if (hash) return hashStreamOf(varRef(hash.localId, hash.type, locOf(expr))); + const streamValue = this.valueStreamReceivers.get(expr); + if (streamValue) + return valueStreamOf(varRef(streamValue.localId, streamValue.type, locOf(expr))); return this.stagedArgument(expr) ?? lowerExpr(this, expr); } diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index ca8c521186..89aa5b0010 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -80,6 +80,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "procStream.write", "child.stdioStream", "stream.unboxDyn", + "stream.handleStream", "stream.pipeDyn", "stream.unpipeDyn", "weakMap.new", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fa78765791..9490ed78ca 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2712,6 +2712,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ argTypes: [DYN, STRING], result: { kind: "object", className: "%Duplex" }, }, + /** The Transform a Cipheriv or Decipheriv reads and writes as a stream + * (+1, created on first use and owned by the cipher). */ + "stream.handleStream": { + argTypes: [DYN], + result: { kind: "object", className: "%Transform" }, + }, /** Checked-dynamic stream slots (dyn-streams.ts): the native stream a * checked-dynamic value boxes (+1) — a runtime stream with the readable * (1) or writable (2) side — or null for any other value. Never throws; @@ -3936,14 +3942,15 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "stream.finishedOpts": { argTypes: [null, F64, F64, BOOL, null], result: VOID }, "stream.finishedOptsSignal": { argTypes: [null, F64, F64, BOOL, DYN, null], result: VOID }, "sp.pipeline": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, - /** pipeline with a run of function stages: [n, at, k, flags, mask, - * s1..sn, stage1..stagek, (next, read, return)?, box?, (signal, - * abort)?] (+ the callback for the stream.* forms): the user's stage - * functions, the frontend's helpers over the pumped generator or a - * promise destination's box, and the pipeline's AbortSignal with its - * abort closure; flags names which groups are present - * (scr_stream_pipeline_stages). A promise destination's promise type is - * sp.pipelineStages' result. */ + /** pipeline with function stages: [n, k, positions, flags, mask, + * s1..sn, stage1..stagek, helpers..., box?, (signal, abort)?] (+ the + * callback for the stream.* forms): the user's stage functions at the + * positions the bitmask names, the frontend's (next, read, return) + * helpers for each pumped generator (pipelinePumpedRuns) and a promise + * destination's box, and the pipeline's AbortSignal with its abort + * closure; flags bits 1-2 give the destination kind and bit 5 the + * signal group (scr_stream_pipeline_stages). A promise destination's + * promise type is sp.pipelineStages' result. */ "sp.pipelineStages": { argTypes: [F64, F64, F64, F64, F64], result: { kind: "promise", inner: VOID }, @@ -4734,3 +4741,15 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** A builtin name exists exactly when its runtime signature exists. */ 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; + 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++; + } + return runs; +} diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 4142b6efe3..7d8626e4b1 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -237,6 +237,7 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "stream.prop", "stream.stdioWritable", "stream.unboxDyn", + "stream.handleStream", "stream.unpipeDyn", "stream.workerStdio", ]); diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 88ccbbc339..9de1b5b622 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -60,7 +60,7 @@ import { DYN_CLASS_PROPERTIES, callSiteReturnType, classValueConstructor } from import { unionWideningTags } from "./analysis.js"; import { alwaysReturns } from "./control-flow.js"; import { dynamicOperationOf, functionTier } from "./tiers.js"; -import { LIB_FN_SIGS } from "./builtin-signatures.js"; +import { LIB_FN_SIGS, pipelinePumpedRuns } from "./builtin-signatures.js"; /** The promisified crypto calls' value kinds besides checked-dynamic. */ const CRYPTO_PROMISE_VALUES: Readonly> = { @@ -5614,23 +5614,34 @@ function validateFunction( e.fn === "stream.pipelineStagesDyn" || e.fn === "sp.pipelineStages" ) { - // (n, at, k, flags, mask, s1..sn, stage1..stagek, (next, read, - // return)?, box?, (signal, abort)?[, cb]): flags bit 0 a source - // function, bits 1-2 the destination, bits 3/4/5 the groups. - const [n, at, k, flags] = e.args + // (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. + const [n, k, positions, flags] = e.args .slice(0, 4) .map((a) => (a?.kind === "numLit" ? a.value : -1)); - const source = (flags! & 1) !== 0; + const total = n! + k!; const destKind = (flags! >> 1) & 3; - const groups = (flags! & 8 ? 3 : 0) + (flags! & 16 ? 1 : 0) + (flags! & 32 ? 2 : 0); + let functions = 0; + for (let j = 0; j < total && j < 53; j++) { + 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 groups = helperCount + (destKind === 2 ? 1 : 0) + (flags! & 32 ? 2 : 0); const cbArgs = e.fn === "sp.pipelineStages" ? 0 : 1; if ( flags! < 0 || n! < 0 || k! < 1 || - (source ? at !== 0 : at! < 1) || - (destKind === 0 ? at! + 0 >= n! : at! !== n!) || - n! + k! < 2 || + positions! < 1 || + total > 52 || + positions! >= 2 ** total || + functions !== k || + (destKind === 2) !== lastIsFunction || + total < 2 || e.args.length !== 5 + n! + k! + groups + cbArgs ) { err(`libCall ${e.fn} count/arity mismatch`, e.loc); @@ -5647,7 +5658,7 @@ function validateFunction( err(`libCall ${e.fn} stage function ${i} must be a func`, e.loc); } at2 += k!; - const helpers = (flags! & 8 ? 3 : 0) + (flags! & 16 ? 1 : 0); + const helpers = helperCount + (destKind === 2 ? 1 : 0); for (let i = at2; i < at2 + helpers; i++) { if (e.args[i]?.type.kind !== "func") err(`libCall ${e.fn} helper ${i} must be a func`, e.loc); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index d8f158ebcb..c05e7f9949 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_v24"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v25"); 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_v24"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v25"); }); 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/7055-cipher-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7055-cipher-streams.ts.json new file mode 100644 index 0000000000..989c7f452f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7055-cipher-streams.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7055-cipher-streams.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7056-cipher-stream-errors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7056-cipher-stream-errors.ts.json new file mode 100644 index 0000000000..16bd2586c0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7056-cipher-stream-errors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7056-cipher-stream-errors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7057-cipher-streams.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7057-cipher-streams.mjs.json new file mode 100644 index 0000000000..1304933aea --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7057-cipher-streams.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7057-cipher-streams.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7058-pipeline-function-stages.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7058-pipeline-function-stages.mjs.json new file mode 100644 index 0000000000..3b9fd5a0f9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7058-pipeline-function-stages.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7058-pipeline-function-stages.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7059-transform-backpressure.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7059-transform-backpressure.ts.json new file mode 100644 index 0000000000..311882a39a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7059-transform-backpressure.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7059-transform-backpressure.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 85c5ffb000..c18a1d080c 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -337,7 +337,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, + runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index b242b78aa5..f0eab2cb5f 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -434,7 +434,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, + runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_cipher.c b/packages/runtime/src/scr_cipher.c index df62a79a32..c8a10a731a 100644 --- a/packages/runtime/src/scr_cipher.c +++ b/packages/runtime/src/scr_cipher.c @@ -9,7 +9,9 @@ * the runtime StringDecoder, which carries partial characters and base64 * groups between calls as Node's does. chacha20-poly1305 follows OpenSSL's * construction, which also accepts AAD after data. Authentication tags are - * compared in constant time. All arguments are borrowed. */ + * compared in constant time. A cipher used as a stream reads and writes + * through its own native Transform, which the stream unit builds on first + * use, as Node's LazyTransform is. All arguments are borrowed. */ #include "scr_runtime.h" #include @@ -103,6 +105,12 @@ typedef struct ScCipher { bool aad_open; /* AAD was added since the last data or padding */ ScrStr *decoder; /* the StringDecoder encoding once string output starts */ double pending; /* the decoder's carried bytes */ + /* The Transform of a cipher used as a stream (+1) and its release + * (scr_stream.c). Its steps hold the cipher, so the cipher is a + * collector-headered object behind a traced handle, and the pair is + * reclaimed as a cycle. */ + void *stream; + void (*stream_release)(void *); } ScCipher; static void *sc_retain(void *h) { @@ -110,16 +118,38 @@ static void *sc_retain(void *h) { return h; } -static void sc_release(void *h) { +static void sc_trace(void *h, ScrTraceVisit visit, void *ctx) { + ScCipher *c = h; + if (c->stream) visit(c->stream, ctx); +} + +/* The collector frees the stream itself; the rest is released here. */ +static void sc_gcfree(void *h) { ScCipher *c = h; - if (--c->rc) return; mbedtls_aes_free(&c->aes); mbedtls_gcm_free(&c->gcm); mbedtls_chacha20_free(&c->chacha); mbedtls_poly1305_free(&c->poly); scr_str_release(c->decoder); mbedtls_platform_zeroize(c, sizeof *c); - free(c); + scr_obj_free_note(); + scr_cyc_free(c); +} + +static void sc_destroy(void *h) { + ScCipher *c = h; + void *stream = c->stream; + c->stream = NULL; + if (stream) c->stream_release(stream); + sc_gcfree(c); +} + +static void sc_release(void *h) { + ScCipher *c = h; + if (--c->rc == 0) { + scr_cyc_dead(c); + scr_rc_destroy(c, sc_destroy); + } else scr_cyc_released(c); } /* ── errors ───────────────────────────────────────────────────────────── */ @@ -532,9 +562,10 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * sc_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYLEN", "Invalid key length"); goto done; } - ScCipher *c = calloc(1, sizeof *c); - if (!c) scr_trap("scriptc: out of memory\n"); + static ScrCycType cyc_type = {&sc_trace, &sc_gcfree, SCR_CYC_KIND_NONE, 0}; + ScCipher *c = scr_cyc_alloc(sizeof *c, &cyc_type); c->rc = 1; + scr_obj_alloc_note(); c->mode = spec->mode; c->decrypt = decrypt; c->active = true; @@ -883,7 +914,8 @@ static bool sc_aead(const ScCipher *c) { return c->mode == SC_GCM || c->mode == SC_CHACHA; } -static ScrDyn *sc_final(ScCipher *c, ScrDyn *output_encoding) { +/* final()'s output bytes (+1 Buffer), or NULL with the error thrown. */ +static ScrBytes *sc_final_bytes(ScCipher *c) { if (!c->active) { sc_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INVALID_STATE", "Invalid state"); return NULL; @@ -953,7 +985,12 @@ static ScrDyn *sc_final(ScCipher *c, ScrDyn *output_encoding) { sc_throw(SCR_ERR_ERROR, NULL, "Unsupported state or unable to authenticate data"); return NULL; } - return sc_output(c, out, output_encoding, true); + return out; +} + +static ScrDyn *sc_final(ScCipher *c, ScrDyn *output_encoding) { + ScrBytes *out = sc_final_bytes(c); + return out ? sc_output(c, out, output_encoding, true) : NULL; } static ScrDyn *sc_set_aad(ScCipher *c, ScrDyn *self, ScrDyn *buffer, ScrDyn *options) { @@ -1055,6 +1092,49 @@ static ScrDyn *sc_set_auth_tag(ScCipher *c, ScrDyn *self, ScrDyn *buffer, ScrDyn return scr_dyn_retain(self); } +/* ── Cipheriv and Decipheriv as streams ─────────────────────────────── + * The stream unit builds a cipher's Transform from these operations + * (ScrDynHandleTransform): each chunk goes through the handle's update(), + * and the end pushes final() or reports its error, as Node's cipher.js + * _transform and _flush do. */ + +static void *sc_stream_get(void *h) { + return ((ScCipher *)h)->stream; +} + +static void sc_stream_set(void *h, void *stream, void (*release)(void *)) { + ScCipher *c = h; + c->stream = stream; + c->stream_release = release; +} + +/* The handle's update(): it throws out of the write, as in Node, once + * the cipher has finished. */ +static ScrBytes *sc_stream_update(void *h, ScrBytes *chunk) { + ScCipher *c = h; + if (!c->active) { + sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); + return NULL; + } + SCR_BYTES_SNAPSHOT(chunk); + return sc_transform(c, chunk->data, chunk->len * scr_bytes_elem_size(chunk->elem)); +} + +static ScrBytes *sc_stream_final(void *h) { + return sc_final_bytes(h); +} + +static const ScrDynHandleTransform sc_transform_ops = { + sc_stream_get, sc_stream_set, sc_stream_update, sc_stream_final, sc_trace, +}; + +static ScrDynHandleOps sc_ops_cipher; +static ScrDynHandleOps sc_ops_decipher; + +static const ScrDynHandleOps *sc_ops_of(const ScCipher *c) { + return c->decrypt ? &sc_ops_decipher : &sc_ops_cipher; +} + static bool sc_name_is(const char *method, const char *name) { return strcmp(method, name) == 0; } @@ -1076,8 +1156,12 @@ static bool sc_stream_member(const char *name) { static void sc_stream_fence(void) { static const char message[] = - "Cipheriv and Decipheriv stream members have no lowering; call update() and final()"; - scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + "This Cipheriv and Decipheriv stream member has no lowering when called through an untyped value"; + static const char streamless[] = + "Cipheriv and Decipheriv stream members called through an untyped value have no lowering in a " + "program that uses no streams"; + if (scr_handle_stream_hooks) scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + else scr_throw_error_msg_code(SCR_ERR_ERROR, streamless, sizeof streamless - 1, "SC2020"); } static ScrDyn *sc_dispatch(ScCipher *c, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, @@ -1091,6 +1175,12 @@ static ScrDyn *sc_dispatch(ScCipher *c, ScrDyn *self, const char *method, ScrDyn if (c->decrypt && sc_name_is(method, "setAuthTag")) return sc_set_auth_tag(c, self, sc_arg(args, argc, 0), sc_arg(args, argc, 1)); if (sc_stream_member(method)) { + /* The members a native stream answers to checked-dynamic calls run + * on the cipher's Transform; the others have no lowering. */ + ScrDyn *result = NULL; + if (scr_handle_stream_hooks && + scr_handle_stream_hooks->call(sc_ops_of(c), c, self, method, args, argc, &result)) + return result; sc_stream_fence(); return NULL; } @@ -1117,11 +1207,19 @@ static ScrDyn *sc_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *cons } static ScrDyn *sc_get(void *h, const char *key, size_t key_len) { - (void)h; (void)key; (void)key_len; + /* The stream properties read the cipher's Transform. */ + if (scr_handle_stream_hooks) { + ScrDyn *value = scr_handle_stream_hooks->get(sc_ops_of(h), h, key, key_len); + if (value || scr_exc_pending()) return value; + } static const char message[] = - "Cipheriv and Decipheriv property reads have no lowering; call update(), final(), setAAD(), " - "setAutoPadding(), getAuthTag() or setAuthTag()"; - scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + "Cipheriv and Decipheriv property reads have no lowering other than the stream properties; call " + "update(), final(), setAAD(), setAutoPadding(), getAuthTag() or setAuthTag()"; + static const char streamless[] = + "Cipheriv and Decipheriv property reads through an untyped value have no lowering in a program " + "that uses no streams"; + if (scr_handle_stream_hooks) scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + else scr_throw_error_msg_code(SCR_ERR_ERROR, streamless, sizeof streamless - 1, "SC2020"); return NULL; } @@ -1132,5 +1230,7 @@ static bool sc_set(void *h, const char *key, size_t key_len, const ScrDyn *value return true; } -static ScrDynHandleOps sc_ops_cipher = {"Cipheriv", sc_retain, sc_release, sc_invoke, sc_get, sc_set, NULL, NULL, NULL}; -static ScrDynHandleOps sc_ops_decipher = {"Decipheriv", sc_retain, sc_release, sc_invoke, sc_get, sc_set, NULL, NULL, NULL}; +static ScrDynHandleOps sc_ops_cipher = {"Cipheriv", sc_retain, sc_release, sc_invoke, sc_get, sc_set, NULL, NULL, NULL, + .transform = &sc_transform_ops}; +static ScrDynHandleOps sc_ops_decipher = {"Decipheriv", sc_retain, sc_release, sc_invoke, sc_get, sc_set, NULL, NULL, NULL, + .transform = &sc_transform_ops}; diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index c29126a4ab..c8fa40b38d 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_v24(void) {} +void scr_runtime_abi_v25(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 94936a8aae..5e52785880 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_v24(void); +void scr_runtime_abi_v25(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 02c4d828f2..cafc8d07e5 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1388,8 +1388,37 @@ typedef struct ScrDynHandleOps { /* Cross-thread structured clone (postMessage, workerData); NULL = * DataCloneError. See ScrDynHandleTransfer. */ const struct ScrDynHandleTransfer *transfer; + /* A handle that is also a Node Transform (Cipheriv, Decipheriv): the + * stream unit builds the handle's Transform from these operations on its + * first stream use. NULL = the class is not a stream. */ + const struct ScrDynHandleTransform *transform; } ScrDynHandleOps; +/* The operations a Transform handle gives the stream unit (scr_stream.c): + * the slot where the handle keeps its stream (+1) with the stream's + * release, update() and final() over bytes (+1 Buffer, or NULL with the + * exception pending), and the handle's collector trace, since the + * stream's steps hold the handle. */ +typedef struct ScrDynHandleTransform { + void *(*stream)(void *h); + void (*set_stream)(void *h, void *stream, void (*release)(void *)); + struct ScrBytes *(*update)(void *h, struct ScrBytes *chunk); + struct ScrBytes *(*final)(void *h); + ScrTraceFn trace; +} ScrDynHandleTransform; + +/* Checked-dynamic member calls and property reads on a Transform handle, + * installed by the stream unit (scr_stream_install); NULL in a program + * without it. call answers true with *result set (+1, or NULL with the + * exception pending) for the members a native stream answers, and get + * answers a stream property (+1), or NULL for other keys. */ +typedef struct ScrHandleStreamHooks { + bool (*call)(const ScrDynHandleOps *ops, void *h, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, ScrDyn **result); + ScrDyn *(*get)(const ScrDynHandleOps *ops, void *h, const char *key, size_t key_len); +} ScrHandleStreamHooks; +extern SCR_TL const ScrHandleStreamHooks *scr_handle_stream_hooks; + /* How a handle crosses threads, supplied by the handle's owning unit so the * message encoder (scr_message.c) needs no reference to gated units and no * per-tag registry. share runs on the sending thread: it answers a payload diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index b8f16630b5..194ade8879 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -37,6 +37,8 @@ static SCR_TL ScrDyn *scr_builtin_object_prototype; +SCR_TL const ScrHandleStreamHooks *scr_handle_stream_hooks; + /* Constructor capsules expose identity and name/length, but do not model * per-class static property tables yet. Never report a fabricated empty view. */ static bool scr_dyn_class_reflection_fence(const ScrDyn *value) { @@ -4309,6 +4311,7 @@ ScrDyn *scr_dyn_new_handle(void *h, ScrDynHandleTag tag) { d->v.handle.tag = tag; d->v.handle.traced = tag == SCR_DYNH_WEAK_MAP || tag == SCR_DYNH_WEAK_SET || tag == SCR_DYNH_WORKER || tag == SCR_DYNH_MESSAGE_PORT || tag == SCR_DYNH_DISPOSABLE_STACK || + tag == SCR_DYNH_CIPHER || tag == SCR_DYNH_DECIPHER || ((tag == SCR_DYNH_SET || tag == SCR_DYNH_MAP) && ((ScrMap *)h)->traced != 0); return d; diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index f7330b6153..271937f2e2 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -175,6 +175,11 @@ struct ScrStreamState { ScrStreamPlainInv flush_inv; bool flush_called; bool flush_pending; /* _flush called, its callback not yet run */ + /* Transform._write's backpressure: the readable length when the + * transform started, and whether its completed write waits for the + * readable side to read (Node's kCallback). */ + size_t xf_length; + bool xf_held; /* A _final that completes at once (zlib's ZlibBase): 'prefinish' starts * the flush and 'finish' does not wait for it. */ bool flush_on_prefinish; @@ -412,6 +417,7 @@ typedef enum { SCR_ST_ERROR_BUFFER, /* errorBuffer: buffered write and end() callbacks */ SCR_ST_CONSTRUCT, /* constructNT: run the construct hook */ SCR_ST_CONSTRUCTED, /* onConstruct: resume reads/writes/destroy */ + SCR_ST_XF_DONE, /* a transform's write callback after push(null) */ } ScrStreamTickOp; typedef struct ScrStreamTick { @@ -688,8 +694,13 @@ static void scr_stream_call_read(ScrStream *s) { } if (st->r.read_cb == NULL) { /* transform reads are push-driven; the attempt still clears the - * initial sync flag (Node's Transform._read ran) */ + * initial sync flag (Node's Transform._read ran) and runs a held + * write callback */ st->r.in_read_sync = false; + if (st->xf_held) { + st->xf_held = false; + scr_stream_write_done(s, NULL); + } return; } st->r.reading = true; @@ -1706,17 +1717,20 @@ static void scr_stream_wq_push(ScrStreamState *st, ScrBytes *chunk /*moves*/, Sc st->w.n++; } +static void scr_stream_transform_callback(ScrStream *s); + static void scr_stream_do_write(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/) { ScrStreamState *st = s->st; st->w.writing = true; st->w.wsync = true; st->w.inflight_len = chunk->len; st->w.inflight_cb = cb; + if (st->is_transform || st->passthrough) st->xf_length = st->r.length; if (st->passthrough && st->transform_cb == NULL) { /* identity: push through, complete synchronously */ scr_stream_add_chunk(s, chunk, false); scr_bytes_release(chunk); - if (!scr_exc_pending()) scr_stream_write_done(s, NULL); + if (!scr_exc_pending()) scr_stream_transform_callback(s); st->w.wsync = false; return; } @@ -3294,13 +3308,13 @@ ScrDyn *scr_stream_from_done(void) { } /* ── pipeline function stages (pipeline.js: makeAsyncIterable, pumpToNode) ── - * A run of async generator functions between two node streams. Each - * stage is called while the pipeline is set up, in pipeline order: - * makeAsyncIterable hands the first one the previous stream itself (a - * Readable is async iterable) and each later one the previous stage's - * generator. The last generator is pumped into the stream after the run - * with pumpToNode. The stages and helpers use the closure ABI (the - * closure first; the callee owns its parameters). */ + * Runs of async generator functions between node streams. Each stage is + * called while the pipeline is set up, in pipeline order: + * makeAsyncIterable hands a stage that follows a stream the stream itself + * (a Readable is async iterable) and any other stage the previous stage's + * generator. The last generator of a run that a stream follows is pumped + * into that stream with pumpToNode. The stages and helpers use the closure + * ABI (the closure first; the callee owns its parameters). */ typedef ScrPromise *(*ScrPipeNextFn)(ScrClosure *, ScrGen *); typedef ScrDyn *(*ScrPipeReadFn)(ScrClosure *, void *); @@ -3309,20 +3323,25 @@ typedef ScrDyn *(*ScrPipeBoxFn)(ScrClosure *, ScrPromise *); enum { SCR_PIPE_DEST_STREAM, SCR_PIPE_DEST_PROMISE = 2 }; typedef struct { - size_t at; /* streams[0..at-1] come before the stages */ size_t count; /* the function stages */ + uint64_t positions; /* bit j: the pipeline's j-th stage is a function */ /* stage(source[, { signal }]) answers an async generator, or a promise * for a promise destination; the source is absent for a source - * function, else the previous stream or async generator */ + * function (position 0), else the previous stream or async generator */ ScrClosure **stages; - bool source; /* stages[0] is the source (at == 0) */ 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 */ - ScrClosure *next, *read, *ret; /* the pumped generator's helpers, or NULL */ + /* (next, read, return) for each generator pumped into a stream, in + * pipeline order, or NULL */ + ScrClosure **helpers; ScrClosure *box; /* SCR_PIPE_DEST_PROMISE: boxes its promise as a dyn promise */ } ScrPipeStages; +static bool scr_pipe_is_stage(const ScrPipeStages *fs, size_t j) { + return fs && j < 64 && ((fs->positions >> j) & 1u); +} + /* stages[i](source[, { signal }]): the callee owns its parameters. */ static void *scr_pipe_call_stage(const ScrPipeStages *fs, size_t i, void *src /*moves*/) { ScrClosure *st = fs->stages[i]; @@ -3331,7 +3350,7 @@ static void *scr_pipe_call_stage(const ScrPipeStages *fs, size_t i, void *src /* opts = scr_dyn_new_obj(); scr_dyn_obj_set(opts, "signal", 6, scr_dyn_retain(fs->signal)); } - if (i == 0 && fs->source) { + if (i == 0 && (fs->positions & 1u)) { return opts ? ((void *(*)(ScrClosure *, ScrDyn *))st->fn)(st, opts) : ((void *(*)(ScrClosure *))st->fn)(st); } @@ -3580,17 +3599,17 @@ static void scr_pump_after_wait(ScrPump *pm, int stage, bool rejected) { } /* pumpToNode(gen, w, finish, { end }): starts at once, as the async - * function does. The helpers are borrowed. */ -static void scr_pump_start(ScrStreamPipeCtx *ctx, ScrGen *gen /*moves*/, const ScrPipeStages *fs, + * function does. The helpers (next, read, return) are borrowed. */ +static void scr_pump_start(ScrStreamPipeCtx *ctx, ScrGen *gen /*moves*/, ScrClosure *const *helpers, ScrStream *w, bool end) { ScrPump *pm = calloc(1, sizeof *pm); if (!pm) scr_stream_oom(); pm->rc = 1; pm->ctx = scr_stream_pipectx_retain_v(ctx); pm->gen = gen; - pm->next = scr_closure_retain(fs->next); - pm->read = scr_closure_retain(fs->read); - pm->ret = scr_closure_retain(fs->ret); + pm->next = scr_closure_retain(helpers[0]); + pm->read = scr_closure_retain(helpers[1]); + pm->ret = scr_closure_retain(helpers[2]); pm->w = scr_stream_retain(w); pm->end = end; pm->drain_l = scr_pump_closure((void *)&scr_pump_drain_l, pm); @@ -3721,27 +3740,27 @@ static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosu ctx->dispose = add(signal, listener); scr_dyn_release(listener); } - /* pipelineImpl's loop over the stages: the n streams, with fs->count - * function stages before streams[fs->at] */ + /* pipelineImpl's loop over the stages: the n streams and fs->count + * function stages, at the positions fs->positions names */ size_t stages = n + (fs ? fs->count : 0); - size_t fn_first = fs ? fs->at : stages, fn_end = fs ? fs->at + fs->count : stages; ScrStream *ret = NULL; ScrGen *ret_gen = NULL; /* +1: ret when the previous stage was a function */ size_t i = 0; /* the next stream */ + size_t k = 0; /* the next function stage */ + size_t h = 0; /* the next pumped generator's helpers */ for (size_t j = 0; j < stages; j++) { bool reading = j + 1 < stages; bool end = reading || end_opt; bool last = j + 1 == stages; - if (j >= fn_first && j < fn_end) { + if (scr_pipe_is_stage(fs, j)) { /* ret = stage({ signal }) for the source, else * stage(makeAsyncIterable(ret), { signal }) */ - size_t k = j - fn_first; bool dest = last && fs->dest != SCR_PIPE_DEST_STREAM; - void *src = k == 0 && fs->source ? NULL - : ret_gen ? (void *)ret_gen - : (void *)scr_stream_retain(ret); + void *src = j == 0 ? NULL + : ret_gen ? (void *)ret_gen + : (void *)scr_stream_retain(ret); ret_gen = NULL; - void *out = scr_pipe_call_stage(fs, k, src); + void *out = scr_pipe_call_stage(fs, k++, src); if (scr_exc_pending()) { if (out && dest) scr_promise_release(out); else if (out) scr_gen_release(out); @@ -3776,7 +3795,7 @@ static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosu } ScrStream *s = streams[i]; bool writing = j > 0; - if (reading && !(j + 1 >= fn_first && j + 1 < fn_end) && + if (reading && !scr_pipe_is_stage(fs, j + 1) && (streams[i + 1]->st->close_emitted || streams[i + 1]->st->destroyed)) { scr_pipe_throw("ERR_STREAM_UNABLE_TO_PIPE", "Cannot pipe to a closed or destroyed stream", SCR_ERR_ERROR); @@ -3803,7 +3822,7 @@ static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosu ctx->finish_count++; ScrGen *g = ret_gen; ret_gen = NULL; - scr_pump_start(ctx, g, fs, s, end); + scr_pump_start(ctx, g, fs->helpers + 3 * h++, s, end); } else if (j > 0) { if (!ret->st->has_r) { static const char msg[] = @@ -3841,12 +3860,12 @@ static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosu return scr_stream_retain(ret); } -static ScrPipeStages scr_pipe_stages_of(double at, double count, ScrClosure **stages, double flags, - double mask, ScrClosure *next, ScrClosure *read, - ScrClosure *ret, ScrClosure *box, ScrDyn *signal) { +static ScrPipeStages scr_pipe_stages_of(double count, ScrClosure **stages, double positions, + double flags, double mask, ScrClosure **helpers, + ScrClosure *box, ScrDyn *signal) { int bits = (int)flags; - ScrPipeStages fs = {(size_t)at, (size_t)count, stages, (bits & 1) != 0, (bits >> 1) & 3, - (unsigned)mask, signal, next, read, ret, box}; + ScrPipeStages fs = {(size_t)count, (uint64_t)positions, stages, (bits >> 1) & 3, (unsigned)mask, + signal, helpers, box}; return fs; } @@ -3864,25 +3883,25 @@ static void scr_pipe_dyn_value_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, if (r != NULL) scr_dyn_release(r); } -/* pipeline(...) with a run of function stages (see ScrPipeStages): flags - * bit 0 marks a source function and bits 1-2 the destination kind; mask - * names the stages taking { signal }. The helpers, box, signal and abort - * are borrowed (each NULL when unused); cb moves. */ -ScrStream *scr_stream_pipeline_stages(double n, ScrStream **streams, double at, double count, - ScrClosure **stages, double flags, double mask, - ScrClosure *next, ScrClosure *read, ScrClosure *ret, - ScrClosure *box, ScrDyn *signal, ScrClosure *abort, - ScrClosure *cb, ScrStreamErrInv inv) { - ScrPipeStages fs = scr_pipe_stages_of(at, count, stages, flags, mask, next, read, ret, box, signal); +/* 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 + * { signal }. The helpers, box, signal and abort are borrowed (each NULL + * when unused); cb moves. */ +ScrStream *scr_stream_pipeline_stages(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + ScrDyn *signal, ScrClosure *abort, ScrClosure *cb, + ScrStreamErrInv inv) { + ScrPipeStages fs = scr_pipe_stages_of(count, stages, positions, flags, mask, helpers, box, signal); return scr_stream_pipeline_ex((size_t)n, streams, cb, inv, true, NULL, NULL, &fs, abort, NULL); } -ScrStream *scr_stream_pipeline_stages_dyn(double n, ScrStream **streams, double at, double count, - ScrClosure **stages, double flags, double mask, - ScrClosure *next, ScrClosure *read, ScrClosure *ret, - ScrClosure *box, ScrDyn *signal, ScrClosure *abort, - ScrDyn *cb) { - ScrPipeStages fs = scr_pipe_stages_of(at, count, stages, flags, mask, next, read, ret, box, signal); +ScrStream *scr_stream_pipeline_stages_dyn(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + ScrDyn *signal, ScrClosure *abort, ScrDyn *cb) { + 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; @@ -4263,12 +4282,12 @@ static void scr_sp_value_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, ScrPr scr_promise_release(p); } -ScrPromise *scr_sp_pipeline_stages(double n, ScrStream **streams, double at, double count, - ScrClosure **stages, double flags, double mask, - ScrClosure *next, ScrClosure *read, ScrClosure *ret, - ScrClosure *box, ScrDyn *signal, ScrClosure *abort) { +ScrPromise *scr_sp_pipeline_stages(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + ScrDyn *signal, ScrClosure *abort) { ScrPromise *p = scr_promise_new(); - ScrPipeStages fs = scr_pipe_stages_of(at, count, stages, flags, mask, next, read, ret, box, signal); + 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, true, NULL, NULL, &fs, abort, vi); @@ -5800,6 +5819,19 @@ void scr_stream_destroy_done(ScrStream *s, ScrError *err /*moves*/) { } } +/* Transform._write's callback after the transform pushed its output: + * on a tick once the readable side has ended, at once when the writable + * side has ended, the transform pushed nothing, or the readable buffer is + * below its high-water mark, and otherwise when the readable side next + * reads (Transform._read). */ +static void scr_stream_transform_callback(ScrStream *s) { + ScrStreamState *st = s->st; + if (st->r.ended) scr_st_tick(s, SCR_ST_XF_DONE, NULL, NULL); + else if (st->w.ending || st->xf_length == st->r.length || st->r.length < st->r.hwm) + scr_stream_write_done(s, NULL); + else st->xf_held = true; +} + void scr_stream_transform_done(ScrStream *s, ScrError *err /*moves*/, ScrBytes *data /*moves*/, ScrStr *data_str /*moves*/) { if (!err) { @@ -5820,7 +5852,8 @@ void scr_stream_transform_done(ScrStream *s, ScrError *err /*moves*/, ScrBytes * if (err) scr_error_release(err); return; } - scr_stream_write_done(s, err); + if (err) scr_stream_write_done(s, err); + else scr_stream_transform_callback(s); } void scr_stream_flush_done(ScrStream *s, ScrError *err /*moves*/, ScrBytes *data /*moves*/, @@ -6171,6 +6204,9 @@ static void scr_stream_run_tick(ScrStreamTick *t) { } break; } + case SCR_ST_XF_DONE: + if (st->w.writing) scr_stream_write_done(s, NULL); + break; case SCR_ST_AFTER_WRITE: { st->w.after_scheduled = false; ScrClosure *cb = t->cb; @@ -6369,8 +6405,11 @@ static void scr_stream_dispatch(void) { } } +static const ScrHandleStreamHooks scr_handle_stream_hooks_impl; + void scr_stream_install(void) { scr_emitter_on_hook = &scr_stream_on_listener; + scr_handle_stream_hooks = &scr_handle_stream_hooks_impl; scr_loop_set_stream(&scr_stream_ticks_pending, &scr_stream_dispatch); } @@ -6409,23 +6448,138 @@ void scr_stream_worker_stdio(void) { #endif } -static ScrStream *scr_stream_view_receiver(const ScrDyn *value) { - if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && - !memcmp(value->v.typed_ref.type_key, "object:", 7)) { - ScrStream *stream = scr_dyn_typed_ref_unbox(value); - const ScrVt *vt = scr_stream_vt_of(stream); - if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || - (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream; - value->v.typed_ref.release(stream); +/* ── handles that are Transforms (Cipheriv, Decipheriv) ────────────────── + * A Transform handle (ScrDynHandleOps.transform) used as a stream gets one + * native Transform, built here on first use and kept in the handle's + * slot. Its steps share the handle (held, and traced: the handle and its + * stream form a cycle the collector reclaims) and the handle's static + * ops: each chunk goes through the handle's update(), and the flush pushes + * final() or reports its error. */ + +static void *scr_handle_ops_retain(void *p) { return p; } +static void scr_handle_ops_release(void *p) { (void)p; } + +static const ScrDynHandleOps *scr_handle_step_handle_ops(ScrClosure *cb) { + return scr_box_get_ref(cb->caps[1]); +} + +/* The step's handle, borrowed: scr_box_get_ref answers +1, and the box + * keeps the handle alive. */ +static void *scr_handle_step_handle(ScrClosure *cb) { + void *h = scr_box_get_ref(cb->caps[0]); + scr_handle_step_handle_ops(cb)->release(h); + return h; +} + +static const ScrDynHandleTransform *scr_handle_step_ops(ScrClosure *cb) { + return scr_handle_step_handle_ops(cb)->transform; +} + +/* The pending exception as a stream error (+1). */ +static ScrError *scr_handle_step_caught(void) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *value = scr_caught_to_dyn(caught); + scr_caught_release(caught); + ScrError *err = value ? scr_errdyn_err_of(value) : NULL; + if (value) scr_dyn_release(value); + return err ? err : scr_stream_mkerr("ERR_INTERNAL_ASSERTION", "a Transform handle threw a value that is not an Error"); +} + +static void scr_handle_step_transform(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { + /* A throwing update() leaves the exception pending: it throws out of + * the write, as Node's _transform does. */ + ScrBytes *out = scr_handle_step_ops(cb)->update(scr_handle_step_handle(cb), chunk); + if (out) scr_stream_transform_done(s, NULL, out, NULL); +} + +static void scr_handle_step_flush(ScrClosure *cb, ScrStream *s) { + ScrBytes *out = scr_handle_step_ops(cb)->final(scr_handle_step_handle(cb)); + scr_stream_flush_done(s, out ? NULL : scr_handle_step_caught(), out, NULL); +} + +static void scr_handle_step_destroy(ScrClosure *cb, ScrStream *s, ScrError *err) { + (void)cb; + scr_stream_destroy_done(s, err ? scr_error_retain(err) : NULL); +} + +static ScrClosure *scr_handle_step(const ScrDynHandleOps *ops, void *h) { + ScrClosure *step = scr_closure_new(NULL, 2); + step->caps[0] = scr_box_new_obj(ops->retain, ops->release, ops->transform->trace); + scr_box_set_ref(step->caps[0], ops->retain(h)); + step->caps[1] = scr_box_new_obj(scr_handle_ops_retain, scr_handle_ops_release, NULL); + scr_box_set_ref(step->caps[1], (void *)ops); + return step; +} + +/* The Transform of handle h (+1), built on first use. */ +static ScrStream *scr_handle_transform(const ScrDynHandleOps *ops, void *h) { + const ScrDynHandleTransform *t = ops->transform; + ScrStream *s = t->stream(h); + if (!s) { + s = scr_stream_new_transform(-1, -1, true, true, true, true, true, + scr_handle_step(ops, h), scr_handle_step_transform, + scr_handle_step(ops, h), scr_handle_step_flush, + scr_handle_step(ops, h), scr_handle_step_destroy); + s->cls = ops->cls; + t->set_stream(h, s, scr_stream_release_v); } + return scr_stream_retain(s); +} + +/* The native stream of a checked-dynamic Transform handle (+1), or NULL + * when the value is not one. */ +ScrStream *scr_stream_of_handle(const ScrDyn *value) { + if (value->kind != SCR_DYN_HANDLE) return NULL; + const ScrDynHandleOps *ops = scr_dyn_handle_ops_of(value); + return ops->transform ? scr_handle_transform(ops, value->v.handle.ptr) : NULL; +} + +/* A boxed native stream (+1), or NULL for any other value. */ +static ScrStream *scr_stream_boxed(const ScrDyn *value) { + if (value->kind != SCR_DYN_TYPED_REF || value->v.typed_ref.type_key_len <= 7 || + memcmp(value->v.typed_ref.type_key, "object:", 7) != 0) + return NULL; + ScrStream *stream = scr_dyn_typed_ref_unbox(value); + const ScrVt *vt = scr_stream_vt_of(stream); + if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || + (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) + return stream; + value->v.typed_ref.release(stream); + return NULL; +} + +/* 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). */ +ScrStream *scr_stream_handle(const ScrDyn *value) { + ScrStream *stream = scr_stream_of_handle(value); + if (!stream) stream = scr_stream_boxed(value); + if (stream) return stream; + static const char message[] = "Stream use of a value that is not a native stream has no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; +} + +static ScrStream *scr_stream_view_receiver(const ScrDyn *value) { + ScrStream *stream = scr_stream_of_handle(value); + if (!stream) stream = scr_stream_boxed(value); + if (stream) return stream; scr_dyn_arg_type_fail("destination", "an instance of Stream", value); return NULL; } -static ScrDyn *scr_stream_view_call(ScrClosure *cb, ScrDyn *const *args, size_t argc) { - ScrStream *stream = scr_box_get_ref(cb->caps[0]); - ScrStr *name = scr_box_get_ref(cb->caps[1]); - const char *method = name->data; +/* The members a native stream answers to checked-dynamic calls. */ +static const char *const scr_stream_dyn_methods[] = { + "on", "once", "addListener", "prependListener", "prependOnceListener", + "off", "removeListener", "pipe", "unpipe", "read", "pause", "resume", "isPaused", + "setEncoding", "write", "end", "destroy", "cork", "uncork", +}; + +/* A checked-dynamic call of one of a native stream's members (borrowed + * stream): the result, +1, with `this` results answering `self`, or the + * current `this` when self is NULL. */ +static ScrDyn *scr_stream_dyn_call(ScrStream *stream, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc) { ScrDyn *result = NULL; ScrDyn *first = argc ? args[0] : scr_dyn_undefined(); if (!strcmp(method, "on") || !strcmp(method, "once") || !strcmp(method, "addListener") || @@ -6501,21 +6655,72 @@ static ScrDyn *scr_stream_view_call(ScrClosure *cb, ScrDyn *const *args, size_t scr_error_release(error); } else if (!strcmp(method, "cork")) scr_stream_cork(stream); else if (!strcmp(method, "uncork")) scr_stream_uncork(stream); - if (!result && !scr_exc_pending()) result = scr_dyn_this_get(); + if (!result && !scr_exc_pending()) result = self ? scr_dyn_retain(self) : scr_dyn_this_get(); + return result; +} + +static ScrDyn *scr_stream_view_call(ScrClosure *cb, ScrDyn *const *args, size_t argc) { + ScrStream *stream = scr_box_get_ref(cb->caps[0]); + ScrStr *name = scr_box_get_ref(cb->caps[1]); + ScrDyn *result = scr_stream_dyn_call(stream, NULL, name->data, args, argc); scr_str_release(name); scr_stream_release(stream); return result; } +/* ScrHandleStreamHooks.call: a member a native stream answers to + * checked-dynamic calls, on the handle's Transform. Members that answer + * the stream answer `self`. */ +static bool scr_handle_stream_call(const ScrDynHandleOps *ops, void *h, ScrDyn *self, + const char *method, ScrDyn *const *args, size_t argc, + ScrDyn **result) { + for (size_t i = 0; i < sizeof scr_stream_dyn_methods / sizeof *scr_stream_dyn_methods; i++) { + if (strcmp(method, scr_stream_dyn_methods[i]) != 0) continue; + ScrStream *stream = scr_handle_transform(ops, h); + *result = scr_stream_dyn_call(stream, self, method, args, argc); + scr_stream_release(stream); + return true; + } + return false; +} + +/* The stream properties a checked-dynamic read answers. */ +static const char *const scr_stream_dyn_properties[] = { + "readable", "readableEnded", "readableLength", "readableHighWaterMark", + "writable", "writableEnded", "writableFinished", "writableLength", "writableHighWaterMark", + "writableNeedDrain", "destroyed", "closed", +}; + +static ScrDyn *scr_stream_dyn_property(ScrStream *stream, const char *name) { + double value = scr_stream_prop(stream, name); + bool number = strstr(name, "Length") || strstr(name, "HighWaterMark"); + return number ? scr_dyn_new_num(value) : scr_dyn_new_bool(value != 0); +} + +/* ScrHandleStreamHooks.get: a stream property of the handle's Transform. */ +static ScrDyn *scr_handle_stream_get(const ScrDynHandleOps *ops, void *h, const char *key, + size_t key_len) { + for (size_t i = 0; i < sizeof scr_stream_dyn_properties / sizeof *scr_stream_dyn_properties; i++) { + const char *name = scr_stream_dyn_properties[i]; + if (strlen(name) != key_len || memcmp(name, key, key_len) != 0) continue; + ScrStream *stream = scr_handle_transform(ops, h); + ScrDyn *value = scr_stream_dyn_property(stream, name); + scr_stream_release(stream); + return value; + } + return NULL; +} + +static const ScrHandleStreamHooks scr_handle_stream_hooks_impl = { + scr_handle_stream_call, + scr_handle_stream_get, +}; + ScrDyn *scr_stream_dyn_view(ScrStream *stream) { ScrDyn *view = scr_dyn_new_obj(); ScrDyn *prototype = scr_dyn_new_obj(); - static const char *const methods[] = { - "on", "once", "addListener", "prependListener", "prependOnceListener", - "off", "removeListener", "pipe", "unpipe", "read", "pause", "resume", "isPaused", - "setEncoding", "write", "end", "destroy", "cork", "uncork", - }; - for (size_t i = 0; i < sizeof methods / sizeof *methods; i++) { + const char *const *methods = scr_stream_dyn_methods; + for (size_t i = 0; i < sizeof scr_stream_dyn_methods / sizeof *scr_stream_dyn_methods; i++) { ScrClosure *cb = scr_closure_new((void *)scr_stream_view_call, 2); cb->caps[0] = scr_box_new_obj(scr_stream_retain_v, scr_stream_release_v, scr_stream_trace); cb->caps[1] = scr_box_new(SCR_BOX_STR); @@ -6524,16 +6729,9 @@ ScrDyn *scr_stream_dyn_view(ScrStream *stream) { scr_dyn_obj_set(prototype, methods[i], strlen(methods[i]), scr_dyn_new_func(cb, scr_stream_view_call, 0, "native stream method", methods[i])); } - static const char *const properties[] = { - "readable", "readableEnded", "readableLength", "readableHighWaterMark", - "writable", "writableEnded", "writableFinished", "writableLength", "writableHighWaterMark", - "writableNeedDrain", "destroyed", "closed", - }; - for (size_t i = 0; i < sizeof properties / sizeof *properties; i++) { - double value = scr_stream_prop(stream, properties[i]); - bool number = strstr(properties[i], "Length") || strstr(properties[i], "HighWaterMark"); - scr_dyn_obj_set(view, properties[i], strlen(properties[i]), - number ? scr_dyn_new_num(value) : scr_dyn_new_bool(value != 0)); + for (size_t i = 0; i < sizeof scr_stream_dyn_properties / sizeof *scr_stream_dyn_properties; i++) { + const char *name = scr_stream_dyn_properties[i]; + scr_dyn_obj_set(view, name, strlen(name), scr_stream_dyn_property(stream, name)); } if (stream->st->native && stream->st->native_ops && stream->st->native_ops->view) { ScrClosure *cb = scr_closure_new((void *)scr_stream_view_call, 2); @@ -6712,17 +6910,13 @@ ScrStream *scr_stream_pipe_general(ScrStream *src, ScrStream *dst, bool end) { /* A pipeline() stage that arrives as a checked-dynamic value (the * stream.unboxDyn libCall): a boxed native stream unwraps by identity - * (+1); any other value throws the compiled program's not-supported - * message (Node also takes iterables and functions there). */ + * (+1), a stream handle (a Cipheriv or Decipheriv) answers its stream, and + * any other value throws the compiled program's not-supported message + * (Node also takes iterables and functions there). */ ScrStream *scr_stream_unbox_dyn(const ScrDyn *value, const ScrStr *message) { - if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && - !memcmp(value->v.typed_ref.type_key, "object:", 7)) { - ScrStream *stream = scr_dyn_typed_ref_unbox(value); - const ScrVt *vt = scr_stream_vt_of(stream); - if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || - (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream; - value->v.typed_ref.release(stream); - } + ScrStream *stream = scr_stream_of_handle(value); + if (!stream) stream = scr_stream_boxed(value); + if (stream) return stream; scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); return NULL; } diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 99f58331c0..1e5b789e71 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -143,6 +143,13 @@ ScrStream *scr_stream_pipe_general(ScrStream *src, ScrStream *dst, bool end); /* A boxed native stream out of a checked-dynamic pipeline stage (+1), or * NULL with `message` thrown as the not-supported Error. */ ScrStream *scr_stream_unbox_dyn(const struct ScrDyn *value, const ScrStr *message); +/* Handles that are Transforms (ScrDynHandleOps.transform: Cipheriv and + * Decipheriv). scr_stream_of_handle: the handle's Transform (+1), built on + * first use, or NULL when the value is not such a handle. + * stream.handleStream: the same, throwing the coded SC2020 error for any + * other value. */ +ScrStream *scr_stream_of_handle(const struct ScrDyn *value); +ScrStream *scr_stream_handle(const struct ScrDyn *value); /* Checked-dynamic stream slots (scr_stream_dyn.c): the native stream a * value boxes with the requested side (1 readable, 2 writable; +1, or * NULL), and readable.pipe()/unpipe() for a destination that is not a @@ -197,35 +204,34 @@ ScrStream *scr_stream_pipeline(double n, ScrStream **streams, ScrClosure *cb, Sc * mustCall wrappers; borrowed). */ ScrClosure *scr_stream_finished_dyn(ScrStream *s, struct ScrDyn *cb); ScrStream *scr_stream_pipeline_dyn(double n, ScrStream **streams, struct ScrDyn *cb); -/* pipeline(...[, cb]) with one run of function stages (pipeline.js): the - * run starts at streams[at] (count functions). flags bit 0: the first - * stage is a source function, called with only its options; bits 1-2: - * the last stage is a function destination (2: an async function whose - * promise, boxed through box, carries the value to the callback), and - * pipeline() answers a PassThrough. Other - * stages get the previous stream itself or the previous stage's async - * generator. mask bit i: stage i takes { signal } (the pipeline's own - * AbortSignal, aborted through abort when the pipeline finishes). The - * last generator before a stream or a PassThrough is pumped - * (pumpToNode) through its helpers: next/ret answer the resume's - * promise, read maps its IteratorResult to the value or - * scr_stream_from_done. Streams and closures borrowed (helpers, box, - * signal and abort NULL when unused); cb moves. */ -ScrStream *scr_stream_pipeline_stages(double n, ScrStream **streams, double at, double count, - ScrClosure **stages, double flags, double mask, - ScrClosure *next, ScrClosure *read, ScrClosure *ret, - ScrClosure *box, struct ScrDyn *signal, ScrClosure *abort, - ScrClosure *cb, ScrStreamErrInv inv); -ScrStream *scr_stream_pipeline_stages_dyn(double n, ScrStream **streams, double at, double count, - ScrClosure **stages, double flags, double mask, - ScrClosure *next, ScrClosure *read, ScrClosure *ret, - ScrClosure *box, struct ScrDyn *signal, - ScrClosure *abort, struct ScrDyn *cb); -struct ScrPromise *scr_sp_pipeline_stages(double n, ScrStream **streams, double at, double count, - ScrClosure **stages, double flags, double mask, - ScrClosure *next, ScrClosure *read, ScrClosure *ret, - ScrClosure *box, struct ScrDyn *signal, - ScrClosure *abort); +/* pipeline(...[, cb]) with function stages (pipeline.js): positions bit j + * marks the j-th of the n + count stages a function. A function at + * position 0 is a source function, called with only its options. flags + * bits 1-2: the last stage is a function destination (2: an async + * function whose promise, boxed through box, carries the value to the + * callback), and pipeline() answers a PassThrough. Other stages get the + * previous stream itself or the previous stage's async generator. mask + * bit i: function i takes { signal } (the pipeline's own AbortSignal, + * aborted through abort when the pipeline finishes). The last generator + * of each run that a stream follows is pumped (pumpToNode) through its + * three helpers in `helpers`: next/ret answer the resume's promise, read + * maps its IteratorResult to the value or scr_stream_from_done. Streams + * and closures borrowed (helpers, box, signal and abort NULL when + * unused); cb moves. */ +ScrStream *scr_stream_pipeline_stages(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + struct ScrDyn *signal, ScrClosure *abort, ScrClosure *cb, + ScrStreamErrInv inv); +ScrStream *scr_stream_pipeline_stages_dyn(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + struct ScrDyn *signal, ScrClosure *abort, + struct ScrDyn *cb); +struct ScrPromise *scr_sp_pipeline_stages(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + struct ScrDyn *signal, ScrClosure *abort); /* stream.compose(s1..sn): a pipeline over the streams and a Duplex that * writes to the first and reads from the last (compose.js). One stream * answers itself (Duplex.from of a duplex). +1, or NULL with diff --git a/packages/runtime/src/scr_stream_dyn.c b/packages/runtime/src/scr_stream_dyn.c index 11dcf6ee26..5cef9d5362 100644 --- a/packages/runtime/src/scr_stream_dyn.c +++ b/packages/runtime/src/scr_stream_dyn.c @@ -17,8 +17,11 @@ static bool scr_stream_dyn_within(const ScrVt *vt, const ScrVt *cls) { /* The native stream a checked-dynamic value boxes (+1), when it has the * requested side (1 readable, 2 writable; Duplex, Transform and PassThrough - * have both), or NULL for any other value. Never throws. */ + * have both), or the stream of a stream handle, or NULL for any other + * value. Never throws. */ ScrStream *scr_stream_dyn_native(const ScrDyn *value, double side) { + /* A Cipheriv or Decipheriv is a Transform. */ + if (value->kind == SCR_DYN_HANDLE) return scr_stream_of_handle(value); if (value->kind != SCR_DYN_TYPED_REF || value->v.typed_ref.type_key_len <= 7 || memcmp(value->v.typed_ref.type_key, "object:", 7) != 0) return NULL; diff --git a/packages/runtime/src/scr_zlib_stream.c b/packages/runtime/src/scr_zlib_stream.c index 4ed717d533..54ffacf02f 100644 --- a/packages/runtime/src/scr_zlib_stream.c +++ b/packages/runtime/src/scr_zlib_stream.c @@ -104,7 +104,8 @@ static void scr_zs_finish(ScrZlibStream *zs, ScrStream *s) { zs->flushing = false; scr_stream_flush_done(s, NULL, NULL, NULL); } else { - scr_stream_transform_done(s, NULL, NULL, NULL); + /* scr_zs_complete has applied Transform._write's callback rule. */ + scr_stream_write_done(s, NULL); } } diff --git a/tests/corpus/7055-cipher-streams.ts b/tests/corpus/7055-cipher-streams.ts new file mode 100644 index 0000000000..8befd23392 --- /dev/null +++ b/tests/corpus/7055-cipher-streams.ts @@ -0,0 +1,182 @@ +// Cipheriv and Decipheriv as Transforms: pipeline() and stream/promises +// pipeline stages, pipe() chains, for await, write()/end() with 'data' +// events, setEncoding(), finished(), compose(), addAbortSignal(), +// stream/consumers, the stream properties, and the cipher's state after +// its stream flushed. A Hash reads through stream/consumers and for await +// the same way. +import * as crypto from "node:crypto"; +import { + PassThrough, + Readable, + Writable, + addAbortSignal, + compose, + finished, + pipeline, +} from "node:stream"; +import { text } from "node:stream/consumers"; +import { pipeline as pipelinePromise } from "node:stream/promises"; + +const key = Buffer.alloc(32, 7); +const iv = Buffer.alloc(16, 3); + +function collector(into: Buffer[]): Writable { + return new Writable({ + write(chunk: Buffer, _enc: string, cb: () => void) { + into.push(chunk); + cb(); + }, + }); +} + +async function viaCallbackPipeline(): Promise { + const out: Buffer[] = []; + await new Promise((resolve) => { + pipeline( + Readable.from(["hello ", "cipher ", "streams"]), + crypto.createCipheriv("aes-256-cbc", key, iv), + crypto.createDecipheriv("aes-256-cbc", key, iv), + collector(out), + (err: Error | null) => { + console.log("callback pipeline:", err ?? null, Buffer.concat(out).toString()); + resolve(); + }, + ); + }); +} + +async function viaPromisePipeline(): Promise { + const hash = crypto.createHash("sha256"); + await pipelinePromise( + Readable.from([Buffer.from("abc"), Buffer.from("def")]), + crypto.createCipheriv("aes-256-ctr", key, iv), + new PassThrough(), + hash, + ); + console.log("promise pipeline digest:", hash.digest("hex")); +} + +async function viaPipeAndForAwait(): Promise { + const cipher = crypto.createCipheriv("aes-256-cbc", key, iv); + const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv); + const chain = Readable.from(["piped ", "through ", "two ciphers"]).pipe(cipher).pipe(decipher); + let text = ""; + for await (const chunk of chain) text += chunk; + console.log("pipe + for await:", text); +} + +async function viaEvents(): Promise { + const cipher = crypto.createCipheriv("aes-128-cbc", Buffer.alloc(16, 1), Buffer.alloc(16, 2)); + cipher.setEncoding("hex"); + let hex = ""; + cipher.on("data", (chunk: string) => { + hex += chunk; + }); + await new Promise((resolve) => { + cipher.on("end", () => { + console.log("events:", typeof hex, hex); + resolve(); + }); + console.log("write returns:", cipher.write("first block of text")); + cipher.end("and the rest"); + }); + // The stream's flush ran final(): the cipher is finished. + try { + cipher.update("more"); + } catch (e) { + console.log("update after the stream:", (e as Error).message); + } + try { + cipher.final(); + } catch (e) { + console.log("final after the stream:", (e as NodeJS.ErrnoException).code, (e as Error).message); + } +} + +async function gcm(): Promise { + const gcmKey = Buffer.alloc(32, 9); + const nonce = Buffer.alloc(12, 4); + const enc = crypto.createCipheriv("aes-256-gcm", gcmKey, nonce); + const parts: Buffer[] = []; + enc.on("data", (chunk: Buffer) => parts.push(chunk)); + await new Promise((resolve) => { + enc.on("end", () => resolve()); + enc.end("authenticated message"); + }); + const tag = enc.getAuthTag(); + console.log("gcm tag length:", tag.length); + const dec = crypto.createDecipheriv("aes-256-gcm", gcmKey, nonce); + dec.setAuthTag(tag); + let plain = ""; + for await (const chunk of Readable.from([Buffer.concat(parts)]).pipe(dec)) plain += chunk; + console.log("gcm round trip:", plain); +} + +async function viaFinishedAndProperties(): Promise { + const c = crypto.createDecipheriv("aes-256-ctr", key, iv); + console.log("before:", c.readable, c.writable, c.writableEnded, c.destroyed, c.writableLength); + const done = new Promise((resolve) => { + finished(c, (err?: NodeJS.ErrnoException | null) => { + console.log("finished:", err ?? null, c.readableEnded, c.writableFinished); + resolve(); + }); + }); + const parts: string[] = []; + const reading = (async () => { + for await (const chunk of c) parts.push(chunk.toString("hex")); + })(); + c.write(Buffer.from("00112233", "hex")); + c.end(Buffer.from("44556677", "hex")); + console.log("ended:", c.writableEnded, c.writable); + await reading; + await done; + console.log("for await over the cipher:", parts.join("")); + await new Promise((resolve) => setImmediate(resolve)); + console.log("after close:", c.destroyed, c.closed); +} + +async function viaCompositions(): Promise { + const both = compose( + crypto.createCipheriv("aes-256-cbc", key, iv), + crypto.createDecipheriv("aes-256-cbc", key, iv), + ); + let composed = ""; + both.on("data", (chunk: Buffer) => { + composed += chunk.toString(); + }); + await new Promise((resolve) => { + both.on("end", () => resolve()); + both.end("composed"); + }); + console.log("compose:", composed); + + const ac = new AbortController(); + const aborted = addAbortSignal(ac.signal, crypto.createCipheriv("aes-256-ctr", key, iv)); + aborted.on("error", (e: Error) => console.log("addAbortSignal:", e.name)); + const closed = new Promise((resolve) => aborted.on("close", () => resolve())); + ac.abort(); + await closed; + console.log("aborted cipher destroyed:", aborted.destroyed); + + const sealed = crypto.createCipheriv("aes-256-ctr", key, iv).update("through consumers"); + const plain = await text(Readable.from([sealed]).pipe(crypto.createDecipheriv("aes-256-ctr", key, iv))); + console.log("consumers text:", plain); + + const hash = crypto.createHash("sha1"); + hash.setEncoding("hex"); + console.log("hash text:", await text(Readable.from(["abc"]).pipe(hash))); + const md5 = crypto.createHash("md5"); + md5.end("xyz"); + for await (const digest of md5) console.log("hash for await:", (digest as Buffer).toString("hex")); +} + +async function main(): Promise { + await viaFinishedAndProperties(); + await viaCompositions(); + await viaCallbackPipeline(); + await viaPromisePipeline(); + await viaPipeAndForAwait(); + await viaEvents(); + await gcm(); +} +main(); diff --git a/tests/corpus/7056-cipher-stream-errors.ts b/tests/corpus/7056-cipher-stream-errors.ts new file mode 100644 index 0000000000..e3e6530913 --- /dev/null +++ b/tests/corpus/7056-cipher-stream-errors.ts @@ -0,0 +1,111 @@ +// Cipher stream errors, close ordering and backpressure: a bad decrypt +// through pipeline() destroys every stage with the same error, a failed +// GCM authentication reaches 'error' after the data, destroy() closes the +// cipher's stream, and write() reports the high-water mark with 'drain'. +import * as crypto from "node:crypto"; +import { Readable, Writable, pipeline } from "node:stream"; + +const key = Buffer.alloc(32, 5); +const iv = Buffer.alloc(16, 6); + +function sealed(text: string): Buffer { + const c = crypto.createCipheriv("aes-256-cbc", key, iv); + return Buffer.concat([c.update(text), c.final()]); +} + +async function badDecrypt(): Promise { + const log: string[] = []; + const source = Readable.from([sealed("secret message")]); + const decipher = crypto.createDecipheriv("aes-256-cbc", Buffer.alloc(32, 8), iv); + const sink = new Writable({ + write(_chunk: Buffer, _enc: string, cb: () => void) { + cb(); + }, + }); + source.on("close", () => log.push("source close")); + decipher.on("error", (e: NodeJS.ErrnoException) => log.push(`decipher error ${e.code}`)); + decipher.on("close", () => log.push("decipher close")); + sink.on("error", (e: NodeJS.ErrnoException) => log.push(`sink error ${e.code}`)); + sink.on("close", () => log.push("sink close")); + await new Promise((resolve) => { + pipeline(source, decipher, sink, (err: NodeJS.ErrnoException | null) => { + log.push(`callback ${err?.code} ${err?.message}`); + setImmediate(() => { + console.log(log.join("\n")); + console.log("destroyed:", source.destroyed, decipher.destroyed, sink.destroyed); + resolve(); + }); + }); + }); +} + +async function gcmWithoutTag(): Promise { + const d = crypto.createDecipheriv("aes-256-gcm", key, Buffer.alloc(12)); + await new Promise((resolve) => { + d.on("data", (chunk: Buffer) => console.log("gcm data", chunk.length)); + d.on("error", (e: Error) => console.log("gcm error:", e.message)); + d.on("close", () => { + console.log("gcm close"); + resolve(); + }); + d.end(Buffer.alloc(5)); + }); +} + +async function destroyMidway(): Promise { + const c = crypto.createCipheriv("aes-256-ctr", key, iv); + await new Promise((resolve) => { + c.on("data", (chunk: Buffer) => { + console.log("ctr data", chunk.length); + c.destroy(new Error("stop")); + }); + c.on("error", (e: Error) => console.log("ctr error:", e.message)); + c.on("close", () => { + console.log("ctr close, destroyed:", c.destroyed); + resolve(); + }); + c.write("0123456789"); + c.write("abcdef"); + }); +} + +async function backpressure(): Promise { + const c = crypto.createCipheriv("aes-256-ctr", key, iv); + let received = 0; + const slow = new Writable({ + highWaterMark: 1024, + write(chunk: Buffer, _enc: string, cb: () => void) { + received += chunk.length; + setImmediate(cb); + }, + }); + c.pipe(slow); + const block = Buffer.alloc(4096, 1); + let writes = 0; + let fullAt = -1; + while (writes < 64) { + writes++; + if (!c.write(block)) { + fullAt = writes; + break; + } + } + console.log("write returned false at", fullAt); + await new Promise((resolve) => c.once("drain", () => resolve())); + console.log("drained"); + await new Promise((resolve) => { + slow.on("finish", () => { + console.log("received", received); + resolve(); + }); + c.end(); + }); +} + +async function main(): Promise { + await badDecrypt(); + await gcmWithoutTag(); + await destroyMidway(); + await backpressure(); +} +main(); diff --git a/tests/corpus/7057-cipher-streams.mjs b/tests/corpus/7057-cipher-streams.mjs new file mode 100644 index 0000000000..2173d29aa1 --- /dev/null +++ b/tests/corpus/7057-cipher-streams.mjs @@ -0,0 +1,85 @@ +// Cipheriv and Decipheriv streams from JavaScript: pipeline() and pipe() +// with cipher stages, a cipher passed through untyped helpers that call +// its stream members dynamically, and cipher stages that reach pipeline() +// through an untyped parameter. +import crypto from "node:crypto"; +import { pipeline, Readable, Writable } from "node:stream"; + +const key = Buffer.alloc(24, 1); +const iv = Buffer.alloc(16, 2); + +function attach(stream, label, log) { + stream.on("data", (chunk) => log.push(`${label} data ${chunk.length}`)); + stream.on("end", () => log.push(`${label} end`)); + stream.on("close", () => log.push(`${label} close`)); + return stream; +} + +function feed(stream, parts) { + for (const part of parts) stream.write(part); + stream.end(); +} + +function state(stream) { + return [stream.readable, stream.writable, stream.writableEnded, stream.destroyed, stream.readableLength].join(","); +} + +const log = []; +const cipher = attach(crypto.createCipheriv("aes-192-cbc", key, iv), "cipher", log); +const collected = []; +cipher.on("data", (chunk) => collected.push(chunk)); +cipher.on("end", () => { + console.log("state at end:", state(cipher)); + const sealed = Buffer.concat(collected); + const out = []; + pipeline( + Readable.from([sealed.subarray(0, 7), sealed.subarray(7)]), + crypto.createDecipheriv("aes-192-cbc", key, iv), + new Writable({ + write(chunk, enc, cb) { + out.push(chunk); + cb(); + }, + }), + (err) => { + console.log(log.join("\n")); + console.log("pipeline:", err ?? null, Buffer.concat(out).toString()); + const piped = Readable.from([sealed]).pipe(crypto.createDecipheriv("aes-192-cbc", key, iv)); + piped.setEncoding("utf8"); + let text = ""; + piped.on("data", (s) => (text += s)); + piped.on("end", () => { + console.log("pipe:", text); + throughHelper(sealed); + }); + }, + ); +}); + +// The stages arrive as untyped values. +function run(stages, done) { + const out = []; + pipeline( + stages[0], + stages[1], + new Writable({ + write(chunk, enc, cb) { + out.push(chunk); + cb(); + }, + }), + (err) => done(err, Buffer.concat(out)), + ); +} + +function throughHelper(sealed) { + run([Readable.from([sealed]), crypto.createDecipheriv("aes-192-cbc", key, iv)], (err, plain) => { + console.log("untyped stage:", err ?? null, plain.toString()); + run([Readable.from([sealed]), crypto.createDecipheriv("aes-192-cbc", Buffer.alloc(24, 9), iv)], (err2) => { + console.log("untyped stage, wrong key:", err2.code, err2.message); + }); + }); +} +console.log("state before:", state(cipher)); +feed(cipher, ["one ", "two ", "three"]); +console.log("state after end():", state(cipher)); diff --git a/tests/corpus/7058-pipeline-function-stages.mjs b/tests/corpus/7058-pipeline-function-stages.mjs new file mode 100644 index 0000000000..f0a61e7c25 --- /dev/null +++ b/tests/corpus/7058-pipeline-function-stages.mjs @@ -0,0 +1,180 @@ +// pipeline() function stages from JavaScript: runs of async generator +// stages between streams, stages named by a function declaration and a +// const, sources that are arrays, strings, synchronous generators and +// generator functions (with the ticks Node's async-from-sync iteration +// takes), the string and Buffer methods of generator-fed chunks, and an +// error thrown in a stage after two streams. +import { pipeline, PassThrough, Transform, Writable } from "node:stream"; +import { pipeline as pipelinePromise } from "node:stream/promises"; +import zlib from "node:zlib"; + +function sink(into) { + return new Writable({ + write(chunk, enc, cb) { + into.push(chunk.toString()); + cb(); + }, + }); +} + +async function* upper(source) { + for await (const chunk of source) yield chunk.toString().toUpperCase(); +} + +const bracket = async function* (source) { + for await (const chunk of source) yield `[${chunk.trim().replaceAll("\n", "|")}]`; +}; + +function* letters() { + yield "a"; + yield "b"; + yield "c"; +} + +// Stages on both sides of two zlib streams, and a named stage. +{ + const out = []; + await pipelinePromise( + async function* () { + for (let i = 0; i < 3; i++) yield `line ${i}\n`; + }, + zlib.createGzip(), + zlib.createGunzip(), + upper, + bracket, + sink(out), + ); + console.log("split runs:", out.join("")); +} + +// Iterable sources, and the destination value that reaches the callback. +await new Promise((resolve) => { + pipeline( + ["x", "y", "z"], + zlib.createDeflate(), + zlib.createInflate(), + async function (source) { + let text = ""; + for await (const chunk of source) text += chunk; + return text; + }, + (err, value) => { + console.log("array source:", err ?? null, value); + resolve(); + }, + ); +}); +{ + const out = []; + await pipelinePromise("str", upper, sink(out)); + console.log("string source:", out.join(",")); + out.length = 0; + await pipelinePromise(letters, upper, sink(out)); + console.log("generator function source:", out.join(",")); + out.length = 0; + await pipelinePromise(letters(), new PassThrough(), sink(out)); + console.log("generator source:", out.join(",")); +} + +// Generator-fed chunks keep their string and Buffer methods. +{ + const out = []; + await pipelinePromise( + async function* () { + yield Buffer.from("buf"); + yield Buffer.from("fers"); + }, + async function* (source) { + for await (const chunk of source) yield chunk.toString("hex"); + }, + async function* (source) { + for await (const chunk of source) yield chunk.padStart(10, "-"); + }, + sink(out), + ); + console.log("chunk methods:", out.join(" ")); +} + +// The ticks of iterable sources match Node's for-await over them. +function ticker(log) { + let on = true; + (async () => { + for (let i = 0; on && i < 12; i++) { + log.push(`t${i}`); + await null; + } + })(); + return () => { + on = false; + }; +} +{ + const log = []; + const stop = ticker(log); + await new Promise((resolve) => + pipeline(["a", "b"], new PassThrough(), sink(log), () => { + stop(); + resolve(); + }), + ); + console.log("array ticks:", log.join(" ")); +} +{ + const log = []; + const stop = ticker(log); + await new Promise((resolve) => + pipeline( + letters, + async function* (source) { + for await (const chunk of source) { + log.push(`stage ${chunk}`); + yield chunk; + } + }, + sink(log), + () => { + stop(); + resolve(); + }, + ), + ); + console.log("generator function ticks:", log.join(" ")); +} + +// An error thrown in a stage after two streams destroys both with it. +{ + const log = []; + const first = new PassThrough(); + const second = new Transform({ + transform(chunk, enc, cb) { + cb(null, chunk.toString().toUpperCase()); + }, + }); + first.on("close", () => log.push("first close")); + second.on("close", () => log.push("second close")); + second.on("error", (e) => log.push(`second error ${e.name}`)); + await new Promise((resolve) => { + pipeline( + async function* () { + yield "one\n"; + yield "two\n"; + }, + first, + second, + async function* (source) { + for await (const chunk of source) { + log.push(`stage got ${JSON.stringify(chunk)}`); + if (chunk.length > 100) yield chunk; + throw new Error("stage failed"); + } + }, + sink([]), + (err) => { + log.push(`callback ${err.message}`); + setImmediate(resolve); + }, + ); + }); + console.log(log.join("\n")); + console.log("destroyed:", first.destroyed, second.destroyed); +} diff --git a/tests/corpus/7059-transform-backpressure.ts b/tests/corpus/7059-transform-backpressure.ts new file mode 100644 index 0000000000..d19a41eb8a --- /dev/null +++ b/tests/corpus/7059-transform-backpressure.ts @@ -0,0 +1,90 @@ +// Transform write backpressure and typed pipeline stages. A Transform +// holds a write's callback while its readable side is at the high-water +// mark, so write() returns false once both buffers fill and 'drain' comes +// after the reader catches up. Typed stages: named async generator +// functions on both sides of zlib streams, an array source, and an async +// function destination whose value resolves the promise pipeline. +import { PassThrough, Transform, Writable } from "node:stream"; +import { pipeline } from "node:stream/promises"; +import * as zlib from "node:zlib"; + +class Copy extends Transform { + override _transform(chunk: Buffer, _enc: BufferEncoding, cb: (err?: Error | null) => void): void { + this.push(chunk); + cb(); + } +} + +async function measure(label: string, t: Transform): Promise { + let received = 0; + const slow = new Writable({ + highWaterMark: 1024, + write(chunk: Buffer, _enc: string, cb: () => void) { + received += chunk.length; + setImmediate(cb); + }, + }); + t.pipe(slow); + const block = Buffer.alloc(4096, 120); + let falseAt = -1; + for (let i = 1; i <= 64; i++) { + if (!t.write(block)) { + falseAt = i; + break; + } + } + console.log(label, "write() false at", falseAt, t.writableLength, t.readableLength); + await new Promise((resolve) => t.once("drain", () => resolve())); + console.log(label, "drained", t.writableLength); + await new Promise((resolve) => { + slow.on("finish", () => resolve()); + t.end(); + }); + console.log(label, "received", received); +} + +async function* numbered(): AsyncGenerator { + for (let i = 0; i < 4; i++) yield `row ${i}\n`; +} + +async function* shout(source: AsyncIterable): AsyncGenerator { + for await (const chunk of source) yield chunk.toString().toUpperCase(); +} + +async function count(source: AsyncIterable): Promise { + let n = 0; + for await (const chunk of source) n += chunk.split("\n").length - 1; + return n; +} + +async function main(): Promise { + await measure("passthrough", new PassThrough()); + await measure("subclass", new Copy()); + await measure( + "options", + new Transform({ + transform(chunk: Buffer, _enc: string, cb: (err?: Error | null, data?: Buffer) => void) { + cb(null, chunk); + }, + }), + ); + + const out: string[] = []; + await pipeline( + numbered, + zlib.createGzip(), + zlib.createGunzip(), + shout, + new Writable({ + write(chunk: Buffer, _enc: string, cb: () => void) { + out.push(chunk.toString()); + cb(); + }, + }), + ); + console.log("named stages:", JSON.stringify(out.join(""))); + + const rows = await pipeline(["a\n", "b\n", "c\n"], zlib.createGzip(), zlib.createGunzip(), shout, count); + console.log("array source, rows:", rows); +} +main(); diff --git a/tests/harness/corpus-costs/7055-cipher-streams.ts.json b/tests/harness/corpus-costs/7055-cipher-streams.ts.json new file mode 100644 index 0000000000..7dda3fe104 --- /dev/null +++ b/tests/harness/corpus-costs/7055-cipher-streams.ts.json @@ -0,0 +1 @@ +{ "plain": 4395, "san": 6600 } diff --git a/tests/harness/corpus-costs/7056-cipher-stream-errors.ts.json b/tests/harness/corpus-costs/7056-cipher-stream-errors.ts.json new file mode 100644 index 0000000000..b3e89de2ab --- /dev/null +++ b/tests/harness/corpus-costs/7056-cipher-stream-errors.ts.json @@ -0,0 +1 @@ +{ "plain": 2558, "san": 4456 } diff --git a/tests/harness/corpus-costs/7057-cipher-streams.mjs.json b/tests/harness/corpus-costs/7057-cipher-streams.mjs.json new file mode 100644 index 0000000000..1edb6e8664 --- /dev/null +++ b/tests/harness/corpus-costs/7057-cipher-streams.mjs.json @@ -0,0 +1 @@ +{ "plain": 2780, "san": 5329 } diff --git a/tests/harness/corpus-costs/7058-pipeline-function-stages.mjs.json b/tests/harness/corpus-costs/7058-pipeline-function-stages.mjs.json new file mode 100644 index 0000000000..d4709c5d64 --- /dev/null +++ b/tests/harness/corpus-costs/7058-pipeline-function-stages.mjs.json @@ -0,0 +1 @@ +{ "plain": 3703, "san": 7279 } diff --git a/tests/harness/corpus-costs/7059-transform-backpressure.ts.json b/tests/harness/corpus-costs/7059-transform-backpressure.ts.json new file mode 100644 index 0000000000..cc34bea0ef --- /dev/null +++ b/tests/harness/corpus-costs/7059-transform-backpressure.ts.json @@ -0,0 +1 @@ +{ "plain": 2394, "san": 5656 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 7da7626b7f..c01b52595f 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -105,7 +105,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, + runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From e7e943a1d3ffd4a8e44fc40ffcd19b217ba8ad4c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:00:06 -0500 Subject: [PATCH 074/395] Accept only nested scans that highestCount walks pass by pass The first pass of guardedLoopCounterFacts accepted a nested while loop anywhere outside an opaque region, and highestCount then left counters unbounded where it does not walk such a loop. The first pass now accepts the same loops highestCount walks: statements of the body, its blocks and its branches. A loop in a labeled block or among an expression's statements is an ordinary loop region, whose counter writes are refused. --- packages/compiler/src/ir/integer-loop-facts.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/ir/integer-loop-facts.ts b/packages/compiler/src/ir/integer-loop-facts.ts index 31edc2f48a..8807212dcc 100644 --- a/packages/compiler/src/ir/integer-loop-facts.ts +++ b/packages/compiler/src/ir/integer-loop-facts.ts @@ -276,16 +276,26 @@ export function guardedLoopCounterFacts( else budget.set(id, (budget.get(id) ?? 0) + delta); }; // Nested while loops outside opaque regions, checked against their own - // conditions once the stable bounds are known. + // conditions once the stable bounds are known. Only loops that highestCount + // walks pass by pass qualify: statements of the body, its blocks and its + // branches. One in a labeled block or among an expression's statements + // (`indirect`) is an ordinary loop region. const nested: (IrStmt & { kind: "while" })[] = []; + let indirect = 0; const expr = (e: IrExpr): boolean => { if (e.kind === "assignExpr") write(e.localId, increment(e.localId, e.value)); if (e.kind === "incDec") write(e.localId, e.op === "+" ? 1 : null); - return everyExprChild(e, expr, stmt); + return everyExprChild(e, expr, (x) => { + indirect++; + const completed = stmt(x); + indirect--; + return completed; + }); }; const condition = new Set(); const stmt = (s: IrStmt): boolean => { - const bounded = s.kind === "while" && opaque === 0 && !s.labels?.length; + const bounded = s.kind === "while" && opaque === 0 && indirect === 0 && !s.labels?.length; + const labeled = s.kind === "block" && !!s.labels?.length; const region = !bounded && (s.kind === "for" || @@ -299,10 +309,12 @@ export function guardedLoopCounterFacts( conditionWrites(s.cond); } if (region) opaque++; + if (labeled) indirect++; if (s.kind === "assign") write(s.localId, increment(s.localId, s.value)); if (s.kind === "varDecl" || s.kind === "forOf") write(s.localId, null); if (s.kind === "tryCatch" && s.catchLocalId) write(s.catchLocalId, null); const completed = everyStmtChild(s, expr, stmt); + if (labeled) indirect--; if (region) opaque--; return completed; }; From 2fbfc1b270d2a48485464ceeb50ac75b28d514c5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:00:30 -0500 Subject: [PATCH 075/395] Keep each build's translation unit private until the build ends The kept LLVM text and IR are named after the entry (`.ll`, `.ir.json`) in the output directory, so two programs whose entries share a name and are built into one directory share those files. The executable cache published the translation unit by copying the shared `.ll` after the native build, so when the other program wrote it in between, the stamp recorded the other program's text, and a later cache hit relinked the wrong program without any error. A build now writes, compiles and caches private copies beside the shared names and renames them into place when it ends. A cache hit that relinks compiles a private copy of the cached translation unit, and leaves a shared file that already holds those bytes as it is. The native build reads the private copy and keeps the shared name as the spelling clang sees, so native cache keys and debug names do not change. --- .../compiler/src/backend/native/contracts.ts | 4 + .../compiler/src/backend/native/executable.ts | 15 +- .../src/executable/executable-cache.test.ts | 11 +- .../src/executable/executable-cache.ts | 48 ++++- packages/compiler/src/index.ts | 176 ++++++++++++------ .../executable-cache-shared-output.test.ts | 102 ++++++++++ 6 files changed, 289 insertions(+), 67 deletions(-) create mode 100644 tests/harness/executable-cache-shared-output.test.ts diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 06a41c9b22..36f804fb26 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -6,6 +6,10 @@ export interface CcOptions { * LLVM backend's .ll — clang compiles IR text natively on the same * command line, so both ride this one seat. */ cPath: string; + /** Where the program TU's bytes are read from, when that is not `cPath`: an invocation-private + * copy that a concurrent build of another program cannot replace. `cPath` stays the spelling + * clang sees (`__FILE__`, debug file names) and the one cache keys name. */ + cSourcePath?: string; /** Path of the native executable to produce. */ outPath: string; /** Additional identity for a translation unit whose complete non-system diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index a551ad600d..9dab1c783c 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -481,7 +481,7 @@ export async function compileCInternal( const [compiler, runtimeHash, programBytes] = await Promise.all([ resolvedTool(driver.argv[0] ?? "clang"), runtimeFingerprint(rtDir), - readFile(opts.cPath), + readFile(opts.cSourcePath ?? opts.cPath), ]); if (compiler !== null) { const effectiveCompiler = @@ -861,7 +861,12 @@ export async function compileCInternal( ); try { await materializeVendorPrerequisites(undefined, privateVendorRoot); - await buildExecutable((p) => p); + await buildExecutable( + (p) => p, + opts.cSourcePath === undefined + ? {} + : { programPath: opts.cSourcePath, compilerVisibleSource: opts.cPath }, + ); } finally { await rm(privateVendorRoot, { recursive: true, force: true }).catch(() => undefined); } @@ -896,7 +901,9 @@ export async function compileCInternal( localArtifact === null ? runtimeFingerprint(rtDir) : Promise.resolve(localArtifact.runtimeHash), - localArtifact === null ? readFile(opts.cPath) : Promise.resolve(localArtifact.programBytes), + localArtifact === null + ? readFile(opts.cSourcePath ?? opts.cPath) + : Promise.resolve(localArtifact.programBytes), ]); compileMetadataStamp = compileMetadataKey === null @@ -965,7 +972,7 @@ export async function compileCInternal( Promise.all([ ccVersion(driver.argv, toolchainEnv, true), runtimeFingerprint(rtDir), - readFile(opts.cPath), + readFile(opts.cSourcePath ?? opts.cPath), ])); } catch (error) { // A version/fingerprint probe is an optimization boundary. If the compiler diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index 94b136c906..e7c57d1b01 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -133,8 +133,15 @@ test("early executable cache restores the emitted TU, IR, and native feature gat const hit = await readEarlyExecutableCache(f.root, f.options); expect(hit?.native).toEqual(native); expect(hit?.executableRestored).toBe(false); - expect(await readFile(f.llvmPath, "utf8")).toBe("; generated llvm\n"); - expect(await readFile(f.irPath, "utf8")).toContain("irVersion"); + // The build compiles private copies beside the shared names and installs + // them there when it is done. + expect(hit?.llvmPath).toBe(f.llvmPath); + expect(hit?.irPath).toBe(f.irPath); + expect(hit?.llvmSourcePath).toMatch(/[/\\]\.scriptc-llvm-/); + expect(hit?.irSourcePath).toMatch(/[/\\]\.scriptc-ir-/); + expect(await readFile(hit!.llvmSourcePath!, "utf8")).toBe("; generated llvm\n"); + expect(await readFile(hit!.irSourcePath!, "utf8")).toContain("irVersion"); + await expect(stat(f.llvmPath)).rejects.toThrow(); expect(await readFile(alternateC, "utf8")).toBe("/* user-owned native source */\n"); }); diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 2312f5e85a..941a7acdcf 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -18,6 +18,7 @@ import { compilerReleaseVersion } from "../library/sidecar.js"; import { nativeArtifactDependenciesStillMatch } from "../backend/native/artifact-stamps.js"; import { type NativeArtifactDependency } from "../backend/native/contracts.js"; import { optimizationKeyParts } from "../backend/optimization.js"; +import { privateSiblingPath } from "../backend/build-cache.js"; import type { CompilerImplementationDependency } from "../library/compiler-self-identity.js"; import { installDarwinDebugSymbols, @@ -179,6 +180,15 @@ interface EarlyExecutableImplementationProof { export interface EarlyExecutableCacheHit { llvmPath: string; irPath?: string; + /** When the executable was not restored: private copies of the cached LLVM text and IR beside + * llvmPath and irPath, which the build compiles from and installs at those names when it is done + * (index.ts BuildSources). A restored executable needs no compile, and its files are installed at + * llvmPath and irPath. */ + llvmSourcePath?: string; + irSourcePath?: string; + /** llvmPath (irPath) already holds the cached bytes: the build leaves it as it is. */ + llvmPathCurrent?: true; + irPathCurrent?: true; /** True when the final executable was restored after native dependencies * validated. False restores only frontend artifacts and must call compileC. */ executableRestored: boolean; @@ -575,15 +585,6 @@ async function readExecutableCache( (await nativeArtifactDependenciesStillMatch(stamp.nativeDependencies)); if (validateRoute !== undefined && !executableRestored) return null; const paths = outputPaths(options, stamp.native.backend); - if (!(await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest))) { - await installBytes(translationUnit, paths.llvmPath); - } - if ( - ir !== null && - stamp.files.ir !== null && - !(await fileMatches(paths.irPath, stamp.files.ir.digest)) - ) - await installBytes(ir, paths.irPath); if (executableRestored) { try { if (debugSymbols !== null) await installDarwinDebugSymbols(debugSymbols, options.outPath); @@ -598,6 +599,31 @@ async function readExecutableCache( executableRestored = false; } } + // A build that compiles from the cached translation unit compiles its own copy: a concurrent + // build of another program with the same entry name may replace the shared names meanwhile. + let llvmSourcePath: string | undefined; + let irSourcePath: string | undefined; + let llvmPathCurrent = false; + let irPathCurrent = false; + if (executableRestored) { + if (!(await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest))) + await installBytes(translationUnit, paths.llvmPath); + if ( + ir !== null && + stamp.files.ir !== null && + !(await fileMatches(paths.irPath, stamp.files.ir.digest)) + ) + await installBytes(ir, paths.irPath); + } else { + llvmSourcePath = privateSiblingPath(paths.llvmPath, "llvm"); + await installBytes(translationUnit, llvmSourcePath); + llvmPathCurrent = await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest); + if (ir !== null && stamp.files.ir !== null) { + irSourcePath = privateSiblingPath(paths.irPath, "ir"); + await installBytes(ir, irSourcePath); + irPathCurrent = await fileMatches(paths.irPath, stamp.files.ir.digest); + } + } const now = new Date(); await Promise.all( [ @@ -619,6 +645,10 @@ async function readExecutableCache( ? { performance: stamp.performance } : {}), ...(ir === null ? {} : { irPath: paths.irPath }), + ...(llvmSourcePath === undefined ? {} : { llvmSourcePath }), + ...(irSourcePath === undefined ? {} : { irSourcePath }), + ...(llvmPathCurrent ? { llvmPathCurrent: true as const } : {}), + ...(irPathCurrent ? { irPathCurrent: true as const } : {}), }; } catch { return null; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 2b216c77de..db1b0b3e34 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -524,6 +524,9 @@ async function compileExecutableNative( onArtifactReady?: NonNullable[0]["onArtifactReady"]>, programPartitions: readonly string[] = [], profileGenerate = false, + /** The kept LLVM text's shared name, when `llvmPath` is the build's private copy of it + * (BuildSources): the spelling clang sees and native cache keys name. */ + llvmVisiblePath?: string, ): Promise { const programIsObject = /\.(?:o|obj)$/.test(llvmPath); const runtimePackTarget = programIsObject && !sanitize ? nativeCodegenTarget() : null; @@ -563,7 +566,9 @@ async function compileExecutableNative( if (programIsObject) throw new InternalCompilerError("program objects must link against a precompiled runtime pack"); await compileExternalC({ - cPath: llvmPath, + ...(llvmVisiblePath === undefined || llvmVisiblePath === llvmPath + ? { cPath: llvmPath } + : { cPath: llvmVisiblePath, cSourcePath: llvmPath }), outPath, cacheIdentity: "scriptc-generated-v1", ...optimizationField(features.optimization), @@ -729,8 +734,12 @@ function runtimePackDiagnostic(error: RuntimePackError, entryPath: string): ScrD interface PreparedExecutable { llvmSource: LlvmInput; + /** The kept LLVM text beside the executable, installed when the build ends. */ llvmPath: string; + /** This build's private copy of the LLVM text (BuildSources). */ + llvmSourcePath: string; irPath: string | undefined; + irSourcePath: string | undefined; nativeFeatures: EarlyExecutableNativeFeatures; programSplit: ReturnType; sourceTexts: Map; @@ -841,6 +850,7 @@ async function prepareExecutableInput( await mkdir(opts.outDir, { recursive: true }); const llvmPath = defaultSourcePaths.llvm; + const llvmSourcePath = privateSiblingPath(llvmPath, "llvm"); const backend = "llvm" as const; const useRuntimePack = opts.nativeProgramObject === true || usesPrecompiledRuntimePack(opts, backend); @@ -863,12 +873,14 @@ async function prepareExecutableInput( return { ok: false, diagnostics: [llvmRefusalDiag(err, entryPath)], sourceTexts }; } timing("llvm-emit"); - await writeLlvmSource(llvmPath, llvmSource); + await writeLlvmSource(llvmSourcePath, llvmSource); timing("llvm-write"); let irPath: string | undefined; + let irSourcePath: string | undefined; if (opts.emitIr) { irPath = defaultSourcePaths.ir; - await writeTextParts(irPath, serializeModuleParts(mod)); + irSourcePath = privateSiblingPath(irPath, "ir"); + await writeTextParts(irSourcePath, serializeModuleParts(mod)); } const nativeFeatures = executableNativeFeatures(mod, backend, opts.optimization ?? "release"); @@ -882,7 +894,51 @@ async function prepareExecutableInput( ? splitLlvmProgram(llvmSource) : null; timing("llvm-split"); - return { llvmSource, llvmPath, irPath, nativeFeatures, programSplit, sourceTexts }; + return { + llvmSource, + llvmPath, + llvmSourcePath, + irPath, + irSourcePath, + nativeFeatures, + programSplit, + sourceTexts, + }; +} + +/** + * The kept LLVM text (`.ll`) and IR (`.ir.json`) sit beside the executable, + * so programs whose entries share a name and are built into one output directory share them. Each + * build therefore writes, compiles and caches private copies (privateSiblingPath) and moves them to + * the shared names only when it is done (rename replaces a file in one step): a concurrent build of + * another program cannot replace the text a build compiles or the translation unit it caches. + */ +interface BuildSources { + llvmPath: string; + llvmSourcePath: string; + irPath?: string | undefined; + irSourcePath?: string | undefined; + /** The shared name already holds the private copy's bytes (a cache hit's restored files): it is + * left as it is, with its time stamps. */ + llvmPathCurrent?: boolean | undefined; + irPathCurrent?: boolean | undefined; +} + +/** Moves a build's private copies to the shared names, or drops them when that fails. */ +async function installBuildSources(sources: BuildSources): Promise { + const install = async ( + source: string | undefined, + destination: string | undefined, + current: boolean | undefined, + ) => { + if (source === undefined || destination === undefined || source === destination) return; + if (current === true) await rm(source, { force: true }); + else await rename(source, destination).catch(() => rm(source, { force: true })); + }; + await Promise.all([ + install(sources.llvmSourcePath, sources.llvmPath, sources.llvmPathCurrent), + install(sources.irSourcePath, sources.irPath, sources.irPathCurrent), + ]); } /** @@ -976,10 +1032,11 @@ type FrontendProcessResult = | { kind: "prepared"; llvmPath: string; - /** A private copy of the LLVM text, which the native helper reads (`nativeInputPath`). */ + /** The build's private copy of the LLVM text (BuildSources), which the native helper reads. */ nativeInputPath: string; llvmBytes: number; irPath: string | undefined; + irSourcePath: string | undefined; nativeFeatures: EarlyExecutableNativeFeatures; sourceTexts: [string, string][]; warnings: ScrDiagnostic[]; @@ -1047,7 +1104,9 @@ async function prepareExecutableInputInChild( return { llvmSource: { file: result.nativeInputPath, bytes: result.llvmBytes }, llvmPath: result.llvmPath, + llvmSourcePath: result.nativeInputPath, irPath: result.irPath, + irSourcePath: result.irSourcePath, nativeFeatures: result.nativeFeatures, programSplit: null, sourceTexts, @@ -1085,18 +1144,15 @@ export async function runFrontendProcess(requestPath: string, resultPath: string ); const frontend = frontendInputs.snapshot(); if (!("ok" in prepared)) { - // The public .ll file may be rewritten by a concurrent build of another entry with the - // same output stem before the native helper reads it (see emitNativeArtifact). if (isLlvmFile(prepared.llvmSource)) throw new InternalCompilerError("in-process preparation returned LLVM text as a file"); - const nativeInputPath = privateSiblingPath(prepared.llvmPath, "frontend-llvm"); - await writeLlvmSource(nativeInputPath, prepared.llvmSource, 0o600); result = { kind: "prepared", llvmPath: prepared.llvmPath, - nativeInputPath, + nativeInputPath: prepared.llvmSourcePath, llvmBytes: llvmInputBytes(prepared.llvmSource), irPath: prepared.irPath, + irSourcePath: prepared.irSourcePath, nativeFeatures: prepared.nativeFeatures, sourceTexts: [...prepared.sourceTexts], warnings: warnings.warnings, @@ -1450,37 +1506,46 @@ async function compileTracked( ...(earlyHit.irPath === undefined ? {} : { irPath: earlyHit.irPath }), }; } - let nativeInputPath = earlyHit.llvmPath; + // The build's private copies of the cached LLVM text and IR (BuildSources). + const hitSources: BuildSources = { + llvmPath: earlyHit.llvmPath, + llvmSourcePath: earlyHit.llvmSourcePath ?? earlyHit.llvmPath, + irPath: earlyHit.irPath, + irSourcePath: earlyHit.irSourcePath, + llvmPathCurrent: earlyHit.llvmPathCurrent, + irPathCurrent: earlyHit.irPathCurrent, + }; + let nativeInputPath = hitSources.llvmSourcePath; let nativeProgramObject: NativeProgramObject | null = null; const useRuntimePack = opts.nativeProgramObject === true || usesPrecompiledRuntimePack(opts, earlyHit.native.backend); - if (useRuntimePack) { - if (earlyHit.native.backend !== "llvm") { - throw new InternalCompilerError( - "native program-object cache hit restored a non-LLVM translation unit", - ); - } - try { - nativeProgramObject = await emitNativeProgramObject( - entryPath, - opts, - // A large program's LLVM exceeds the engine's string limit as one - // string; the emitter takes it in parts, as on a cold build. - await readTextParts(earlyHit.llvmPath), - profile, - ); - nativeInputPath = nativeProgramObject.linkPath; - } catch (err) { - if (!(err instanceof NativeCodegenError)) throw err; - return { - ok: false, - diagnostics: [nativeCodegenDiag(err.diagnosticCode, err.message, entryPath)], - sourceTexts: new Map(), - }; - } - } try { + if (useRuntimePack) { + if (earlyHit.native.backend !== "llvm") { + throw new InternalCompilerError( + "native program-object cache hit restored a non-LLVM translation unit", + ); + } + try { + nativeProgramObject = await emitNativeProgramObject( + entryPath, + opts, + // A large program's LLVM exceeds the engine's string limit as one + // string; the emitter takes it in parts, as on a cold build. + await readTextParts(hitSources.llvmSourcePath), + profile, + ); + nativeInputPath = nativeProgramObject.linkPath; + } catch (err) { + if (!(err instanceof NativeCodegenError)) throw err; + return { + ok: false, + diagnostics: [nativeCodegenDiag(err.diagnosticCode, err.message, entryPath)], + sourceTexts: new Map(), + }; + } + } await compileExecutableNative( earlyHit.native, nativeInputPath, @@ -1496,6 +1561,10 @@ async function compileTracked( : async ({ dependencies }) => { await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, { ...earlyHit, + llvmPath: hitSources.llvmSourcePath, + ...(hitSources.irSourcePath === undefined + ? {} + : { irPath: hitSources.irSourcePath }), executableRestored: true, nativeDependencies: dependencies, frontend: earlyHit.frontend, @@ -1503,6 +1572,7 @@ async function compileTracked( }, nativeProgramObject?.partitionPaths, profile?.generate !== undefined, + hitSources.llvmPath, ); if (nativeProgramObject !== null && opts.nativeProgramObject === true) { await rename(nativeProgramObject.linkPath, nativeProgramObject.artifactPath); @@ -1532,6 +1602,7 @@ async function compileTracked( nativeProgramObject.partitionPaths, ); } + await installBuildSources(hitSources); } await pruneBuildCache(cacheRoot); return { @@ -1564,7 +1635,8 @@ async function compileTracked( ) : await prepareExecutableInput(entryPath, opts, ffi, buildPlatform, timing, warningSink); if ("ok" in prepared) return prepared; - const { llvmSource, llvmPath, irPath, nativeFeatures, programSplit, sourceTexts } = prepared; + const { llvmSource, llvmPath, llvmSourcePath, irPath, irSourcePath } = prepared; + const { nativeFeatures, programSplit, sourceTexts } = prepared; const backend = "llvm" as const; await mkdir(dirname(opts.outPath), { recursive: true }); if (earlyCacheOptions === null) { @@ -1599,7 +1671,7 @@ async function compileTracked( } await compileExecutableNative( nativeFeatures, - nativeProgramObject?.linkPath ?? llvmPath, + nativeProgramObject?.linkPath ?? llvmSourcePath, opts.outPath, opts.sanitize ?? false, ffi, @@ -1611,23 +1683,34 @@ async function compileTracked( ? undefined : async ({ dependencies }) => { await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, { - llvmPath, + llvmPath: llvmSourcePath, native: nativeFeatures, executableRestored: true, nativeDependencies: dependencies, frontend: frontendInputs.snapshot(), - ...(irPath === undefined ? {} : { irPath }), + ...(irSourcePath === undefined ? {} : { irPath: irSourcePath }), ...performanceWarnings(warningSink), }); publishedExecutable = true; }, nativeProgramObject?.partitionPaths, profile?.generate !== undefined, + llvmPath, ); timing("native-link"); if (nativeProgramObject !== null && opts.nativeProgramObject === true) { await rename(nativeProgramObject.linkPath, nativeProgramObject.artifactPath); } + if (!publishedExecutable) { + await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, { + llvmPath: llvmSourcePath, + native: nativeFeatures, + executableRestored: false, + frontend: frontendInputs.snapshot(), + ...(irSourcePath === undefined ? {} : { irPath: irSourcePath }), + ...performanceWarnings(warningSink), + }).catch(() => undefined); + } } catch (err) { if (err instanceof RuntimePackError) { return { ok: false, diagnostics: [runtimePackDiagnostic(err, entryPath)], sourceTexts }; @@ -1649,18 +1732,7 @@ async function compileTracked( nativeProgramObject.partitionPaths, ); } - // The frontend process's private copy of the LLVM text. - if (isLlvmFile(llvmSource)) await rm(llvmSource.file, { force: true }); - } - if (!publishedExecutable) { - await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, { - llvmPath, - native: nativeFeatures, - executableRestored: false, - frontend: frontendInputs.snapshot(), - ...(irPath === undefined ? {} : { irPath }), - ...performanceWarnings(warningSink), - }).catch(() => undefined); + await installBuildSources({ llvmPath, llvmSourcePath, irPath, irSourcePath }); } await pruneBuildCache(cacheRoot); timing("complete"); diff --git a/tests/harness/executable-cache-shared-output.test.ts b/tests/harness/executable-cache-shared-output.test.ts new file mode 100644 index 0000000000..bf9e7353c8 --- /dev/null +++ b/tests/harness/executable-cache-shared-output.test.ts @@ -0,0 +1,102 @@ +/* Two programs whose entries share a name (main.ts) built into one output + * directory share the kept `main.ll` beside their executables. Each build + * must still compile, link and cache its own translation unit: a later + * build that restores either program from the executable cache, or relinks + * it from the cached translation unit, gets that program back. + * + * The overlap is made deterministic with a linker wrapper that rewrites the + * kept `main.ll` with the other program's text while the first build links, + * which is what a concurrent build of the other program does when it writes + * its kept LLVM in that window. A caller-selected linker never publishes the + * final executable, so every cache hit relinks from the cached translation + * unit. Both builds also run concurrently. */ +import { execFile, execFileSync } from "node:child_process"; +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const cli = join(repoRoot, "packages/cli/dist/main.js"); +const windows = process.platform === "win32"; + +let directory = ""; + +beforeAll(() => { + directory = mkdtempSync(join(windows ? tmpdir() : "/tmp", "scriptc-shared-output-")); + for (const name of ["first", "second"]) { + mkdirSync(join(directory, name)); + writeFileSync(join(directory, name, "main.ts"), `console.log("${name} program");\n`); + } + mkdirSync(join(directory, "out")); +}); + +afterAll(() => { + if (directory !== "") rmSync(directory, { recursive: true, force: true }); +}); + +function outPath(name: string): string { + return join(directory, "out", name + (windows ? ".exe" : "")); +} + +async function build(name: string, env: NodeJS.ProcessEnv = {}): Promise { + await execFileAsync( + process.execPath, + [cli, "build", join(directory, name, "main.ts"), "-o", outPath(name)], + { + cwd: repoRoot, + env: { ...process.env, SCRIPTC_CACHE_DIR: join(directory, "cache"), ...env }, + maxBuffer: 1 << 24, + }, + ); +} + +function run(name: string): string { + return execFileSync(outPath(name), { encoding: "utf8" }); +} + +describe.skipIf(windows)("executables built into one output directory", () => { + test( + "keep their own translation units when their builds overlap", + { timeout: 600_000 }, + async () => { + const keptLlvm = join(directory, "out", "main.ll"); + // The second program's kept LLVM, for the wrapper to write while the + // first program links. + await build("second"); + const secondLlvm = join(directory, "second.ll"); + writeFileSync(secondLlvm, readFileSync(keptLlvm)); + const linker = join(directory, "linker.sh"); + writeFileSync( + linker, + [ + "#!/bin/sh", + `if [ -n "$SCRIPTC_TEST_OVERLAP" ]; then cp "${secondLlvm}" "${keptLlvm}"; fi`, + 'exec clang "$@"', + "", + ].join("\n"), + ); + chmodSync(linker, 0o755); + + await build("first", { SCRIPTC_LINKER: linker, SCRIPTC_TEST_OVERLAP: "1" }); + expect(run("first")).toBe("first program\n"); + expect(readFileSync(keptLlvm, "utf8")).toContain("first program"); + // A cache hit relinks from the translation unit the build cached. + rmSync(outPath("first")); + await build("first", { SCRIPTC_LINKER: linker }); + expect(run("first")).toBe("first program\n"); + expect(readFileSync(keptLlvm, "utf8")).toContain("first program"); + + // Both programs at once, cold and then from the cache. + for (const round of [0, 1]) { + if (round === 1) for (const name of ["first", "second"]) rmSync(outPath(name)); + const env = { SCRIPTC_CACHE_DIR: join(directory, "cache-both"), SCRIPTC_LINKER: linker }; + await Promise.all([build("first", env), build("second", env)]); + expect(run("first")).toBe("first program\n"); + expect(run("second")).toBe("second program\n"); + } + }, + ); +}); From 491480e1ad3990c5d6f834e6afbd910420a34ee3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:09:43 -0500 Subject: [PATCH 076/395] Convert clamped stores inline and test thread-owned views once in typed-array loops A Uint8ClampedArray store called scr_bytes_to_u8_clamp for every element, which kept loops of clamped stores scalar. The store now computes ToUint8Clamp in line: NaN and values at most 0 store 0, values at least 255 store 255, and values between round through (v + 2^52) - 2^52, which rounds a value in (0, 255) to the nearest integer, ties to even, as the runtime function does. In worker-enabled programs, every typed-array element access tests the view's shared storage and branches between a relaxed atomic access and the ordinary one, so loops over thread-owned views kept one branch per access. A view's shared storage is fixed at construction, like its length, so the typed-array loop version guard now tests it once for each invariant receiver, and the fast copy reads and writes those views directly. In worker programs a loop is versioned for this even when no bound needs a guard. When any receiver is shared, the checked copy runs unchanged. Programs without workers emit the same code as before. Adds a Uint8ClampedArray brighten kernel to the typed-array-kernels workload. --- benchmarks/runtime/README.md | 2 +- .../runtime/workloads/typed-array-kernels.ts | 12 +++ .../backend/llvm/bytes-loop-versions.test.ts | 31 ++++++++ .../src/backend/llvm/bytes-loop-versions.ts | 46 ++++++++++- .../src/backend/llvm/emit-function.ts | 1 + .../src/backend/llvm/emit-statements.ts | 5 ++ packages/compiler/src/backend/llvm/emitter.ts | 11 ++- .../compiler/src/backend/llvm/expr-bytes.ts | 52 ++++++++++--- .../test/bytes-element-emission.test.ts | 42 ++++++++++ .../6994-uint8-clamped-store-rounding.ts.json | 6 ++ ...5-worker-typed-array-loop-versions.ts.json | 6 ++ .../6994-uint8-clamped-store-rounding.ts | 77 +++++++++++++++++++ .../6995-worker-typed-array-loop-versions.ts | 74 ++++++++++++++++++ .../6994-uint8-clamped-store-rounding.ts.json | 1 + ...5-worker-typed-array-loop-versions.ts.json | 1 + 15 files changed, 348 insertions(+), 19 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6994-uint8-clamped-store-rounding.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6995-worker-typed-array-loop-versions.ts.json create mode 100644 tests/corpus/6994-uint8-clamped-store-rounding.ts create mode 100644 tests/corpus/6995-worker-typed-array-loop-versions.ts create mode 100644 tests/harness/corpus-costs/6994-uint8-clamped-store-rounding.ts.json create mode 100644 tests/harness/corpus-costs/6995-worker-typed-array-loop-versions.ts.json diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 1acdc9ce7c..cbf47675ad 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -26,7 +26,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds | | `template-render` | cpu | template literals, escaping, `map`/`join` | | `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread | -| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, RGBA to gray | +| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray | | `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage | | `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts | | `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains | diff --git a/benchmarks/runtime/workloads/typed-array-kernels.ts b/benchmarks/runtime/workloads/typed-array-kernels.ts index 87c227bbc3..ba68f7241b 100644 --- a/benchmarks/runtime/workloads/typed-array-kernels.ts +++ b/benchmarks/runtime/workloads/typed-array-kernels.ts @@ -17,6 +17,10 @@ function addWrap(a: Int32Array, b: Int32Array, out: Int32Array): void { for (let i = 0; i < out.length; i++) out[i] = (a[i]! + b[i]!) | 0; } +function brighten(src: Uint8ClampedArray, gain: number, out: Uint8ClampedArray): void { + for (let i = 0; i < out.length; i++) out[i] = src[i]! * gain + 8; +} + function gray(rgba: Uint8Array, out: Uint8Array): void { for (let i = 0; i < out.length; i++) { const j = i * 4; @@ -38,6 +42,8 @@ const masked = new Uint8Array(n); const ia = new Int32Array(n); const ib = new Int32Array(n); const io = new Int32Array(n); +const pixels = new Uint8ClampedArray(n); +const bright = new Uint8ClampedArray(n); const rgba = new Uint8Array(n * 4); const luma = new Uint8Array(n); let state = 2463534242; @@ -56,6 +62,7 @@ for (let i = 0; i < n; i++) { key[i] = next() & 255; ia[i] = next() | 0; ib[i] = next() | 0; + pixels[i] = next() & 255; } for (let i = 0; i < rgba.length; i++) rgba[i] = next() & 255; @@ -64,6 +71,7 @@ let f64Sum = 0; let f32Sum = 0; let byteSum = 0; let intSum = 0; +let brightSum = 0; let lumaSum = 0; for (let r = 0; r < rounds; r++) { // Change one input element per round so no round repeats the last. @@ -72,21 +80,25 @@ for (let r = 0; r < rounds; r++) { fa[at] = r; data[at] = r & 255; ia[at] = r * 65537; + pixels[at] = r & 255; rgba[at * 4] = r & 255; axpy(1.5, x, y, d); blend((r & 15) / 16, fa, fb, fo); mask(data, key, masked); addWrap(ia, ib, io); + brighten(pixels, 0.75 + (r & 7) / 8, bright); gray(rgba, luma); const probe = (r * 104729) & (n - 1); f64Sum += d[probe]!; f32Sum += fo[probe]!; byteSum += masked[probe]!; intSum = (intSum + io[probe]!) | 0; + brightSum += bright[probe]!; lumaSum += luma[probe]!; } console.log(`float64 ${f64Sum.toFixed(3)}`); console.log(`float32 ${f32Sum.toFixed(3)}`); console.log(`uint8 ${byteSum}`); console.log(`int32 ${intSum}`); +console.log(`clamped ${brightSum}`); console.log(`gray ${lumaSum}`); 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 ea568bc3f8..3fbc01cc51 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -109,6 +109,8 @@ function match( ranges, initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(), proven: new Set(), + workers: false, + owned: new Set(), }); } @@ -250,6 +252,35 @@ test("loops to a typed array's length version the other typed arrays they index" expect(rebound).toContain("bytes.index.invalid"); }); +test("worker programs test each receiver's shared storage once in the guard", () => { + // for (let i = 0; i < n; i++) out[i] = src[i] * 2, where either view may + // be shared. + const loop = counted([ + set(ref("out", f64s), ref("i"), bin("*", get(ref("src", f64s), ref("i")), num(2))), + ]); + const kernel = fn([loop]); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + workers: true, + 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\.(fast|slow)\d+:$/m)); + expect(guard.match(/icmp eq ptr %\S+, null/g)).toHaveLength(2); + const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0]; + expect(fast).not.toContain("bytes.shared"); + expect(fast).not.toMatch(/(load|store) atomic double/); + // The checked copy keeps the shared-memory path for each access. + const slow = body.slice(body.search(/^bytes\.version\.slow\d+:$/m), body.indexOf(fast)); + expect(slow).toContain("load atomic double"); + expect(slow).toContain("store atomic double"); +}); + 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 df8977426f..dcb735e619 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -6,7 +6,7 @@ import { byteNumberAccess } from "../../ir/byte-numbers.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { exactInteger, widenInteger } from "./integer-values.js"; import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js"; -import { emitBytesHeaderLength } from "./expr-bytes.js"; +import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js"; type Node = IrExpr | IrStmt; type VarRef = IrExpr & { kind: "varRef" }; @@ -41,8 +41,13 @@ interface AffineCheck { export interface BytesLoopVersion { /** Typed-array accesses that are in bounds whenever the guard holds. */ bounds: ReadonlySet; + /** 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 }[]; + /** Receivers the guard requires to have no shared storage. */ + ownedReceivers: number[]; fixed: FixedCheck[]; affine: AffineCheck[]; /** The counted loop whose iteration space the affine checks cover. */ @@ -66,6 +71,11 @@ export interface BytesLoopFacts { initialized: ReadonlySet; /** Accesses already proved in bounds without a guard. */ proven: ReadonlySet; + /** Worker programs: an access of a view that may be shared tests the + * view's shared storage first. */ + workers: boolean; + /** Accesses already proved thread-owned by an enclosing version. */ + owned: ReadonlySet; } const MAX_COST = 400; @@ -87,6 +97,11 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]) * and last values in bounds, such as `out[base + i]` for `i < n` or * `view.getUint32(i * 4)`. * + * 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 + * and writes thread-owned views directly. + * * Only small synchronous loops are versioned (nested loops included), and * the emitter bounds how deeply versions nest. The checked loop runs * whenever the guard fails, and keeps every out-of-bounds read, ignored @@ -314,10 +329,19 @@ export function matchBytesLoopVersion( } return index; }; + // Worker programs: accesses through invariant receivers, proven or not, + // become thread-owned when the guard finds no shared storage. + const owned = new Map(); const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => { - if (facts.proven.has(node)) return; const ref = root(array); if (!ref) return; + if (facts.workers && !facts.owned.has(node)) { + const receiver = receiverOf(ref); + const nodes = owned.get(receiver); + if (nodes) nodes.push(node); + else owned.set(receiver, [node]); + } + if (facts.proven.has(node)) return; const shape = counter !== null ? affine(index) : null; if (shape && shape.counter !== 0) { const operands = new Set(); @@ -383,9 +407,15 @@ export function matchBytesLoopVersion( }); const fixedKept = [...fixed.values()].slice(0, MAX_CHECKS); const affineKept = affineGroups.slice(0, MAX_CHECKS - fixedKept.length); - if (fixedKept.length === 0 && affineKept.length === 0) return null; + const ownedKept = [...owned.entries()].slice(0, MAX_CHECKS); + if (fixedKept.length === 0 && affineKept.length === 0 && ownedKept.length === 0) return null; const bounds = new Set(); + const ownedNodes = new Set(); const used = new Set(); + for (const [receiver, nodes] of ownedKept) { + used.add(receiver); + for (const node of nodes) ownedNodes.add(node); + } const ranges = new Map(); const counterLow = Math.max(counted?.range.min ?? 0, -bound), counterHigh = Math.min(counted?.range.max ?? 0, bound); @@ -420,7 +450,9 @@ export function matchBytesLoopVersion( (counted.range.min < -bound || counted.range.max > bound); return { bounds, + owned: ownedNodes, receivers: order.map((index) => receivers[index]!), + ownedReceivers: ownedKept.map(([receiver]) => renumber.get(receiver)!), fixed: fixedKept.map((g) => ({ receiver: renumber.get(g.check.receiver)!, end: g.check.end, @@ -439,7 +471,8 @@ export function matchBytesLoopVersion( } /** Branches to `fast` when every access the version proves is in bounds for - * every iteration, and to `slow` otherwise (including a counted loop whose + * every iteration and every view it proves thread-owned has no shared + * storage, and to `slow` otherwise (including a counted loop whose * integer form is not established, or a module receiver not yet set). */ export function emitBytesLoopVersionGuard( host: LlvmEmitterContext, @@ -473,6 +506,11 @@ export function emitBytesLoopVersionGuard( return wide; }); const conditions: string[] = []; + for (const index of version.ownedReceivers) { + const owned = B.tmp(); + B.line(`${owned} = icmp eq ptr ${emitBytesSharedStorage(host, receivers[index]!)}, null`); + conditions.push(owned); + } for (const check of version.fixed) { const ok = B.tmp(); B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`); diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index d83b9ff645..5053c1b540 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -228,6 +228,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.integerLoopRanges = rangeFacts.loopRanges; emitter.int32Elements = slotFacts.elements ?? null; emitter.bytesBounds = findBytesBounds(numericFn, emitter.integerRanges); + emitter.bytesOwned = new Set(); // Debug builds keep every number local a double for the debugger's // variable view. Lazily boxed captured parameters keep their double, // which the box is created from, and so do plain copies of a capture or diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 4330296519..d2e96994bf 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -517,6 +517,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { s.index, s.value, emitter.bytesBounds.has(s), + emitter.bytesOwned.has(s), ); break; } @@ -970,6 +971,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { 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 { @@ -978,6 +980,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { 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); @@ -985,6 +989,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.br(done); } finally { emitter.bytesBounds = originalBounds; + emitter.bytesOwned = originalOwned; emitter.integerRanges = originalRanges; emitter.provenCountedGuards.delete(s); emitter.bytesLoopVersioning.delete(s); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 1f2be44391..c254d09077 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1125,6 +1125,9 @@ export class LlEmitter { * guard already passed. */ readonly provenCountedGuards = new Set(); bytesBounds: ReadonlySet = new Set(); + /** Worker programs: typed-array accesses whose views an enclosing version + * guard proved thread-owned. */ + bytesOwned: ReadonlySet = new Set(); integerViews = new Map(); /** Module globals the current function owns outright (initializer * bindings): their function-local slot, which stands in for the global. */ @@ -3836,6 +3839,8 @@ export class LlEmitter { ranges: this.integerRanges, initialized, proven: this.bytesBounds, + workers: this.mod.workers === true, + owned: this.bytesOwned, }); } @@ -6196,8 +6201,9 @@ export class LlEmitter { expr?: IrExpr, inBounds = false, invalidNaN = false, + owned = false, ): LlValue { - return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN); + return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN, owned); } emitToUint32(value: string, expr?: IrExpr, uint32?: string): string { @@ -6260,8 +6266,9 @@ export class LlEmitter { indexExpr?: IrExpr, expr?: IrExpr, inBounds = false, + owned = false, ): void { - return emitBytesSet(this, elem, receiver, index, value, indexExpr, expr, inBounds); + return emitBytesSet(this, elem, receiver, index, value, indexExpr, expr, inBounds, owned); } emitBytesIntrinsic(e: IrExpr & { kind: "bytesIntrinsic" }): LlValue { diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index d36001bf6a..062bfdfbe8 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -176,6 +176,17 @@ export function emitBytesLength( return { name: out, type: F64 }; } +/** A view's shared backing (null for a buffer this thread owns). It is + * set at construction, before script code can see the view. */ +export function emitBytesSharedStorage(host: LlvmEmitterContext, receiver: string): string { + const B = host.B; + const slot = B.tmp(), + storage = B.tmp(); + B.line(`${slot} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 9`); + B.line(`${storage} = load ptr, ptr ${slot}${bytesHeaderAlias(host)}`); + return storage; +} + /** Worker programs: an element of a view of a SharedArrayBuffer (`shared` is * fixed before script code can see the view) is read and written with one * relaxed atomic access of its width, as the runtime's shared accessors do; @@ -186,12 +197,9 @@ export function emitBytesLength( function emitSharedBytesSplit(host: LlvmEmitterContext, receiver: string): string { const B = host.B; host.declare(`declare i1 @llvm.expect.i1(i1, i1)`); - const slot = B.tmp(), - storage = B.tmp(), + const storage = emitBytesSharedStorage(host, receiver), isShared = B.tmp(), expected = B.tmp(); - B.line(`${slot} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 9`); - B.line(`${storage} = load ptr, ptr ${slot}${bytesHeaderAlias(host)}`); B.line(`${isShared} = icmp ne ptr ${storage}, null`); B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${isShared}, i1 false)`); const shared = B.newLabel("bytes.shared"), @@ -209,15 +217,17 @@ export function emitBytesGet( expr?: IrExpr, inBounds = false, invalidNaN = false, + owned = false, ): LlValue { const B = host.B; // Any invalid index traps (the array runtime's discipline). - if (!invalidNaN) return emitBytesLoad(host, elem, receiver, index, expr, inBounds, undefined); + if (!invalidNaN) + return emitBytesLoad(host, elem, receiver, index, expr, inBounds, undefined, owned); // The numeric view of an ordinary read: an invalid index never touches // storage and reads as NaN — ToNumber of Node's undefined. const invalid = B.newLabel("bytes.read.invalid"); const done = B.newLabel("bytes.read.done"); - const loaded = emitBytesLoad(host, elem, receiver, index, expr, inBounds, invalid); + const loaded = emitBytesLoad(host, elem, receiver, index, expr, inBounds, invalid, owned); const ok = B.newLabel("bytes.read.ok"); B.br(ok); B.startBlock(ok); @@ -260,8 +270,10 @@ function emitBytesLoad( expr: IrExpr | undefined, inBounds: boolean, invalid: string | undefined, + owned: boolean, ): LlValue { - if (host.mod.workers) { + // `owned`: an enclosing version guard proved the view thread-owned. + if (host.mod.workers && !owned) { const idx = emitBytesIndex(host, receiver, index, expr, invalid, inBounds); return emitSharedOrLocalLoad(host, elem, receiver, idx); } @@ -515,13 +527,14 @@ export function emitBytesSet( indexExpr?: IrExpr, expr?: IrExpr, inBounds = false, + owned = false, ): void { const B = host.B; // Invalid integer-indexed writes are ignored after evaluating the RHS. const done = B.newLabel("bytes.store.done"); const data = host.emitBytesData(receiver); const idx = emitBytesIndex(host, receiver, index, indexExpr, done, inBounds); - if (host.mod.workers && elem === "f16") { + if (host.mod.workers && !owned && elem === "f16") { host.declare(`declare void @scr_bytes_shared_set(ptr, ${host.sizeType}, double)`); const local = emitSharedBytesSplit(host, receiver); B.line( @@ -532,10 +545,24 @@ export function emitBytesSet( } let stored: string | null; if (elem === "u8c") { - host.declare(`declare double @scr_bytes_to_u8_clamp(double)`); - const clamped = B.tmp(); + // ToUint8Clamp, as scr_bytes_to_u8_clamp computes it: NaN and values + // at most 0 store 0, values at least 255 store 255, and values between + // round to the nearest integer, ties to even. Adding and subtracting + // 2^52 rounds a value in (0, 255) that way in the default rounding mode, + // with no call or branch, so loops of clamped stores can vectorize. + const positive = B.tmp(), + high = B.tmp(), + shifted = B.tmp(), + rounded = B.tmp(), + upper = B.tmp(), + clamped = B.tmp(); stored = B.tmp(); - B.line(`${clamped} = call double @scr_bytes_to_u8_clamp(double ${value.name})`); + B.line(`${positive} = fcmp ogt double ${value.name}, 0.0`); + B.line(`${high} = fcmp oge double ${value.name}, 255.0`); + B.line(`${shifted} = fadd double ${value.name}, 0x4330000000000000`); + B.line(`${rounded} = fsub double ${shifted}, 0x4330000000000000`); + B.line(`${upper} = select i1 ${high}, double 255.0, double ${rounded}`); + B.line(`${clamped} = select i1 ${positive}, double ${upper}, double 0.0`); B.line(`${stored} = fptoui double ${clamped} to i32`); } else stored = @@ -572,7 +599,7 @@ export function emitBytesSet( ty = "i32"; operand = stored!; } - if (host.mod.workers) { + if (host.mod.workers && !owned) { // A shared view's element takes a relaxed atomic store of its width. const local = emitSharedBytesSplit(host, receiver); const p = B.tmp(); @@ -721,6 +748,7 @@ export function emitBytesIntrinsic( e.args[0], host.bytesBounds.has(e), e.invalidNaN === true, + host.bytesOwned.has(e), ); case "slice": return call( diff --git a/packages/compiler/test/bytes-element-emission.test.ts b/packages/compiler/test/bytes-element-emission.test.ts index 505b519638..c0a78a8986 100644 --- a/packages/compiler/test/bytes-element-emission.test.ts +++ b/packages/compiler/test/bytes-element-emission.test.ts @@ -406,3 +406,45 @@ test("large record clones stay outlined while small clones remain inlineable", ( expect(large).toContain("define internal ptr @sc_rclone_large(ptr %src) #2"); expect(large).toContain("attributes #2 = { noinline sanitize_address }"); }); + +test("Uint8ClampedArray stores clamp and round without a runtime call", () => { + const type = bytesOf("u8c"); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + { + name: "store", + params: [ + { localId: "px", name: "px", type }, + { localId: "v", name: "v", type: F64 }, + ], + returnType: VOID, + locals: [ + { id: "px", name: "px", type, mutable: false }, + { id: "v", name: "v", type: F64, mutable: false }, + ], + body: [ + { + kind: "bytesSet", + arr: { kind: "varRef", localId: "px", type, loc }, + index: { kind: "numLit", value: 0, type: F64, loc }, + value: { kind: "varRef", localId: "v", type: F64, loc }, + loc, + }, + ], + loc, + }, + ], + }; + expect(validateModule(mod)).toEqual([]); + const body = /^define internal [^\n]*@sc_bf_store[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + expect(body).not.toContain("scr_bytes_to_u8_clamp"); + expect(body).toMatch(/fcmp ogt double %\S+, 0\.0/); + expect(body).toMatch(/fcmp oge double %\S+, 255\.0/); + expect(body).toMatch(/fadd double %\S+, 0x4330000000000000/); + expect(body).toMatch(/fsub double %\S+, 0x4330000000000000/); + expect(body).toMatch(/store i8 [^\n]*, align 1/); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6994-uint8-clamped-store-rounding.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6994-uint8-clamped-store-rounding.ts.json new file mode 100644 index 0000000000..f5a6b8742d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6994-uint8-clamped-store-rounding.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6994-uint8-clamped-store-rounding.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6995-worker-typed-array-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6995-worker-typed-array-loop-versions.ts.json new file mode 100644 index 0000000000..8078248f57 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6995-worker-typed-array-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6995-worker-typed-array-loop-versions.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6994-uint8-clamped-store-rounding.ts b/tests/corpus/6994-uint8-clamped-store-rounding.ts new file mode 100644 index 0000000000..5fc3295dac --- /dev/null +++ b/tests/corpus/6994-uint8-clamped-store-rounding.ts @@ -0,0 +1,77 @@ +// Uint8ClampedArray stores: NaN and values at most 0 store 0, values at +// least 255 store 255, and values between round to the nearest integer, +// ties to even. Covers single stores and loops, every tie between 0 and +// 255 with the doubles on either side of it, and values far outside. + +/** The largest power of two at most `x` (x > 0). */ +function floorPower(x: number): number { + let p = 1; + while (p * 2 <= x) p *= 2; + while (p > x) p /= 2; + return p; +} + +/** The doubles just above and below `x` (x >= 0). */ +function nextUp(x: number): number { + return x === 0 ? 5e-324 : x + floorPower(x) * 2 ** -52; +} + +function nextDown(x: number): number { + if (x === 0) return -5e-324; + const p = floorPower(x); + return x - (p === x ? p * 2 ** -53 : p * 2 ** -52); +} + +function storeAll(values: Float64Array, out: Uint8ClampedArray): void { + const n = out.length; + for (let i = 0; i < n; i++) out[i] = values[i]!; +} + +function scale(px: Uint8ClampedArray, k: number): void { + const n = px.length; + for (let i = 0; i < n; i++) px[i] = px[i]! * k; +} + +const special = [ + NaN, -NaN, Infinity, -Infinity, 0, -0, 5e-324, -5e-324, 2.2250738585072014e-308, 0.49999999999999994, + 0.5, 0.5000000000000001, 1, 1.5, 2.5, 3.5, 127.5, 128.5, 253.5, 254.49999999999997, 254.5, + 254.50000000000003, 254.99999999999997, 255, 255.00000000000003, 255.5, 256, -1, -0.5, -0.4, + 1e300, -1e300, 2 ** 31, 2 ** 32 + 0.5, 2 ** 52, 2 ** 52 + 1, 2 ** 53, -(2 ** 52), +]; +const one = new Uint8ClampedArray(1); +const singles: number[] = []; +for (const v of special) { + one[0] = v; + singles.push(one[0]!); +} +console.log("single", singles.join(",")); + +const ties: number[] = []; +for (let k = 0; k <= 255; k++) { + ties.push(k + 0.5, nextDown(k + 0.5), nextUp(k + 0.5), k, nextDown(k), nextUp(k)); +} +const values = new Float64Array(ties.length + special.length); +values.set(ties); +values.set(special, ties.length); +const stored = new Uint8ClampedArray(values.length); +storeAll(values, stored); +const expected = new Uint8ClampedArray(values.length); +for (let i = 0; i < values.length; i++) expected[i] = values[i]!; +let mismatches = 0; +for (let i = 0; i < values.length; i++) if (stored[i] !== expected[i]) mismatches++; +console.log("loop", values.length, "mismatches", mismatches); +let digest = 0; +for (let i = 0; i < stored.length; i++) digest = (Math.imul(digest, 31) + stored[i]!) | 0; +console.log("digest", digest, stored.subarray(0, 24).join(",")); +console.log("tail", stored.subarray(ties.length).join(",")); + +const px = new Uint8ClampedArray(64); +for (let i = 0; i < px.length; i++) px[i] = i * 4.25; +scale(px, 1.37); +console.log("scaled", px.join(",")); +scale(px, -1); +console.log("negated", px.join(",")); +const fromInts = new Uint8ClampedArray(8); +const ints = new Int32Array([-300, -1, 0, 1, 254, 255, 256, 2147483647]); +for (let i = 0; i < fromInts.length; i++) fromInts[i] = ints[i]!; +console.log("ints", fromInts.join(",")); diff --git a/tests/corpus/6995-worker-typed-array-loop-versions.ts b/tests/corpus/6995-worker-typed-array-loop-versions.ts new file mode 100644 index 0000000000..5b85271b90 --- /dev/null +++ b/tests/corpus/6995-worker-typed-array-loop-versions.ts @@ -0,0 +1,74 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +// Typed-array loops in a worker-enabled program, over views of a thread's +// own ArrayBuffer, views of a SharedArrayBuffer, and both in one loop. Each +// loop gives the same results whichever kind of buffer its views use, and +// writes through shared views are visible to the other thread. + +function scale(src: Float64Array, dst: Float32Array, k: number): void { + for (let i = 0; i < dst.length; i++) dst[i] = src[i]! * k; +} + +function countAbove(view: Int16Array, limit: number): number { + let n = 0; + for (let i = 0; i < view.length; i++) if (view[i]! > limit) n++; + return n; +} + +function mix(a: Uint8Array, b: Uint8Array, out: Uint8ClampedArray): void { + const n = out.length; + for (let i = 0; i < n; i++) out[i] = a[i]! * 2 - b[i]!; +} + +function bump(view: Int32Array, by: number): void { + for (let i = 0; i < view.length; i++) view[i] = (view[i]! + by) | 0; +} + +function report(label: string, src: Float64Array, dst: Float32Array, ints: Int16Array, a: Uint8Array, b: Uint8Array, out: Uint8ClampedArray): string { + for (let i = 0; i < src.length; i++) src[i] = i * 1.5 - 4; + for (let i = 0; i < ints.length; i++) ints[i] = (i * 7919) % 2001 - 1000; + for (let i = 0; i < a.length; i++) { + a[i] = i * 37; + b[i] = 255 - i * 11; + } + scale(src, dst, 0.1); + mix(a, b, out); + return `${label}: ${dst.join(",")} | ${countAbove(ints, 250)} ${countAbove(ints.subarray(3), -1)} | ${out.join(",")}`; +} + +function views(buffer: ArrayBuffer | SharedArrayBuffer, other: ArrayBuffer | SharedArrayBuffer): string { + const src = new Float64Array(buffer, 0, 12); + const dst = new Float32Array(other, 0, 10); + const ints = new Int16Array(buffer, 96, 40); + const a = new Uint8Array(other, 40, 16); + const b = new Uint8Array(buffer, 176, 16); + const out = new Uint8ClampedArray(other, 56, 16); + return report("pair", src, dst, ints, a, b, out); +} + +if (isMainThread) { + // Bytes 224-255 of the shared buffer hold counters the views never use. + const shared = new SharedArrayBuffer(256); + console.log(views(new ArrayBuffer(256), new ArrayBuffer(256))); + console.log(views(shared, new ArrayBuffer(256))); + console.log(views(new ArrayBuffer(256), shared)); + console.log(views(shared, new SharedArrayBuffer(256))); + const own = new Int32Array(8); + bump(own, 5); + bump(new Int32Array(shared, 224, 8), 1); + const worker = new Worker(new URL(import.meta.url), { workerData: shared }); + worker.on("message", (line: string) => console.log(line)); + worker.on("exit", (code: number) => { + const seen = new Int32Array(shared, 224, 8); + bump(seen, 1000); + console.log(`exit ${code}: ${seen.join(",")} own ${own.join(",")}`); + console.log(`dst after worker: ${new Float32Array(shared, 0, 10).join(",")}`); + }); +} else { + const shared = workerData as SharedArrayBuffer; + const counters = new Int32Array(shared, 224, 8); + bump(counters, 10); + bump(counters.subarray(2, 5), -3); + parentPort!.postMessage(`worker: ${counters.join(",")}`); + parentPort!.postMessage(views(new ArrayBuffer(256), shared)); +} diff --git a/tests/harness/corpus-costs/6994-uint8-clamped-store-rounding.ts.json b/tests/harness/corpus-costs/6994-uint8-clamped-store-rounding.ts.json new file mode 100644 index 0000000000..a52f2c4549 --- /dev/null +++ b/tests/harness/corpus-costs/6994-uint8-clamped-store-rounding.ts.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 3300 } diff --git a/tests/harness/corpus-costs/6995-worker-typed-array-loop-versions.ts.json b/tests/harness/corpus-costs/6995-worker-typed-array-loop-versions.ts.json new file mode 100644 index 0000000000..c0aef3bb72 --- /dev/null +++ b/tests/harness/corpus-costs/6995-worker-typed-array-loop-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 2700, "san": 3900 } From 16cf12c20d66aa23a18308446871e7d55dc18d01 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:11:54 -0500 Subject: [PATCH 077/395] Run two-counter for loops on integer counters `for (let lo = a, hi = b; lo < hi; lo++, hi--)` kept both counters as doubles: the counted-loop match takes a single counter, so every `a[lo]`/`a[hi]` converted and tested its index. A loop whose two counters close in on each other by positive literal steps, are not written by the body and index something now stores both as i64: on every body entry both lie between the first start and the second. A start without a proven integer range is tested at run time on a versioned loop, as a counted loop's is. --- .../src/backend/llvm/counted-loops.ts | 81 ++++++++----- .../src/backend/llvm/emit-statements.ts | 58 ++++++++- .../compiler/src/ir/integer-loops.test.ts | 73 ++++++++++- packages/compiler/src/ir/integer-loops.ts | 114 ++++++++++++++++++ .../corpus/7140-paired-loop-counters.ts.json | 6 + tests/corpus/7140-paired-loop-counters.ts | 81 +++++++++++++ .../7140-paired-loop-counters.ts.json | 1 + 7 files changed, 377 insertions(+), 37 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json create mode 100644 tests/corpus/7140-paired-loop-counters.ts create mode 100644 tests/harness/corpus-costs/7140-paired-loop-counters.ts.json diff --git a/packages/compiler/src/backend/llvm/counted-loops.ts b/packages/compiler/src/backend/llvm/counted-loops.ts index 36fc63e1ff..02943b8523 100644 --- a/packages/compiler/src/backend/llvm/counted-loops.ts +++ b/packages/compiler/src/backend/llvm/counted-loops.ts @@ -1,4 +1,5 @@ -import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; +import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; +import type { IrExpr } from "../../ir/ir.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { f64Lit } from "./common.js"; @@ -59,37 +60,51 @@ export function emitCountedLoopSafety( `${safe} = fcmp ${loop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(loop.guardLimit)}`, ); } - if (loop.startGuarded) { - // An exact integer of at most 2^53 - 1 in magnitude, and not -0 - // (the bit patterns of the value and its round trip agree). - // NaN and infinities fail the ordered range tests. - const start = host.emitExpr(loop.start); - const low = B.tmp(), - high = B.tmp(), - raw = B.tmp(), - wide = B.tmp(), - back = B.tmp(), - bits = B.tmp(), - backBits = B.tmp(), - exact = B.tmp(), - inRange = B.tmp(), - ok = B.tmp(); - B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`); - B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`); - B.line(`${inRange} = and i1 ${low}, ${high}`); - B.line(`${raw} = fptosi double ${start.name} to i64`); - B.line(`${wide} = freeze i64 ${raw}`); - B.line(`${back} = sitofp i64 ${wide} to double`); - B.line(`${bits} = bitcast double ${start.name} to i64`); - B.line(`${backBits} = bitcast double ${back} to i64`); - B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); - B.line(`${ok} = and i1 ${inRange}, ${exact}`); - if (safe === "true") safe = ok; - else { - const both = B.tmp(); - B.line(`${both} = and i1 ${safe}, ${ok}`); - safe = both; - } - } + if (loop.startGuarded) safe = andSafe(host, safe, emitExactStart(host, loop.start)); + return safe; +} + +/** The run-time test of a guarded paired loop: each unproven start is an + * exact safe integer. */ +export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPairedForLoop): string { + let safe = "true"; + for (const counter of [loop.low, loop.high]) + if (counter.startGuarded) safe = andSafe(host, safe, emitExactStart(host, counter.start)); return safe; } + +function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string { + if (safe === "true") return ok; + const both = host.B.tmp(); + host.B.line(`${both} = and i1 ${safe}, ${ok}`); + return both; +} + +/** An exact integer of at most 2^53 - 1 in magnitude, and not -0 (the bit + * patterns of the value and its round trip agree). NaN and infinities fail + * the ordered range tests. */ +function emitExactStart(host: LlvmEmitterContext, expr: IrExpr): string { + const B = host.B; + const start = host.emitExpr(expr); + const low = B.tmp(), + high = B.tmp(), + raw = B.tmp(), + wide = B.tmp(), + back = B.tmp(), + bits = B.tmp(), + backBits = B.tmp(), + exact = B.tmp(), + inRange = B.tmp(), + ok = B.tmp(); + B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`); + B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`); + B.line(`${inRange} = and i1 ${low}, ${high}`); + B.line(`${raw} = fptosi double ${start.name} to i64`); + B.line(`${wide} = freeze i64 ${raw}`); + B.line(`${back} = sitofp i64 ${wide} to double`); + B.line(`${bits} = bitcast double ${start.name} to i64`); + B.line(`${backBits} = bitcast double ${back} to i64`); + B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); + B.line(`${ok} = and i1 ${inRange}, ${exact}`); + return ok; +} diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 4330296519..cf83fc9455 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -42,10 +42,15 @@ import { matchIntegerArrayForLoop, matchIntegerBytesForLoop, matchIntegerCountedForLoop, + matchIntegerPairedForLoop, } from "../../ir/integer-loops.js"; import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js"; import { countedDenseShape, emitDenseLoopGuard, matchDenseArrayLoop } from "./dense-array-loops.js"; -import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js"; +import { + emitCountedLoopLimit, + emitCountedLoopSafety, + emitPairedLoopSafety, +} from "./counted-loops.js"; import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js"; import { mangleLocal } from "../mangle.js"; import { LlvmUnsupportedError } from "./unsupported.js"; @@ -865,6 +870,12 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.unaliasedArrays, ) : null; + // Two counters closing in on each other (`lo++, hi--`) take the same + // integer storage as a counted loop's one. + const pairedLoop = + !integerLoop && !countedLoop && emitter.countedLoopsEnabled + ? matchIntegerPairedForLoop(s, emitter.numericLocals, emitter.integerRanges) + : null; const window = countedLoop && emitter.byteWindowsEnabled ? matchByteWindow( @@ -993,8 +1004,10 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { break; } let countedJoin: string | null = null; - if (countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) { - const safe = emitCountedLoopSafety(emitter, countedLoop); + if ((countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) || pairedLoop?.guarded) { + const safe = countedLoop + ? emitCountedLoopSafety(emitter, countedLoop) + : emitPairedLoopSafety(emitter, pairedLoop!); const fast = B.newLabel("counted.fast"); const slow = B.newLabel("counted.slow"); countedJoin = B.newLabel("counted.done"); @@ -1009,6 +1022,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.scopes.push([]); let integerSlot: string | null = null; let countedLimit: string | null = null; + const pairedSlots: string[] = []; if (integerLoop) { integerSlot = B.slot(); B.entryAllocas.push( @@ -1053,6 +1067,24 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { : countedLoop.range, }); countedLimit = emitCountedLoopLimit(emitter, countedLoop); + } else if (pairedLoop) { + for (const counter of [pairedLoop.low, pairedLoop.high]) { + const slot = B.slot(); + B.entryAllocas.push( + `${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(counter.localId)!.name}`, + ); + const value = emitter.emitExpr(counter.start); + const start = B.tmp(); + B.line(`${start} = fptosi double ${value.name} to i64`); + B.line(`store i64 ${start}, ptr ${slot}`); + pairedSlots.push(slot); + emitter.integerLoopBindings.set(counter.localId, { + slot, + type: "i64", + signed: true, + range: pairedLoop.range, + }); + } } else if (s.init) { // A multi-declarator head shares the loop's scope. Emitting its // IR block as an ordinary block would release captured/ref locals @@ -1134,6 +1166,14 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { `${inBounds} = icmp ${countedLoop.step > 0 ? "slt" : "sgt"} i64 ${index}, ${countedLimit}`, ); B.condBr(inBounds, lb, exit); + } else if (pairedLoop) { + const low = B.tmp(), + high = B.tmp(), + open = B.tmp(); + B.line(`${low} = load i64, ptr ${pairedSlots[0]}`); + B.line(`${high} = load i64, ptr ${pairedSlots[1]}`); + B.line(`${open} = icmp ${pairedLoop.inclusive ? "sle" : "slt"} i64 ${low}, ${high}`); + B.condBr(open, lb, exit); } else if (s.cond) B.condBr(emitter.emitCondition(s.cond), lb, exit); else B.br(lb); B.startBlock(lb); @@ -1165,6 +1205,14 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.line(`${old} = load i64, ptr ${integerSlot}`); B.line(`${next} = add nsw i64 ${old}, ${countedLoop.step}`); B.line(`store i64 ${next}, ptr ${integerSlot}`); + } else if (pairedLoop) { + [pairedLoop.low, pairedLoop.high].forEach((counter, i) => { + const old = B.tmp(); + const next = B.tmp(); + B.line(`${old} = load i64, ptr ${pairedSlots[i]}`); + B.line(`${next} = add nsw i64 ${old}, ${counter.step}`); + B.line(`store i64 ${next}, ptr ${pairedSlots[i]}`); + }); } else if (s.update) emitter.emitStmt(s.update); B.br(lc); } @@ -1173,6 +1221,10 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.releaseScope(emitter.scopes.pop()!); if (integerLoop) emitter.integerLoopBindings.delete(integerLoop.localId); if (countedLoop) emitter.integerLoopBindings.delete(countedLoop.localId); + if (pairedLoop) { + emitter.integerLoopBindings.delete(pairedLoop.low.localId); + emitter.integerLoopBindings.delete(pairedLoop.high.localId); + } if (integerArrayLoop) emitter.integerArrayBindings.delete(integerArrayLoop.localId); if (countedJoin) { B.br(countedJoin); diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index aada3b0535..24bbf5be0f 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -1,6 +1,10 @@ import { expect, test } from "vitest"; import { BOOL, F64, STRING, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "./ir.js"; -import { matchIntegerArrayForLoop, matchIntegerCountedForLoop } from "./integer-loops.js"; +import { + matchIntegerArrayForLoop, + matchIntegerCountedForLoop, + matchIntegerPairedForLoop, +} from "./integer-loops.js"; const loc = { file: "integer-array-loop.ts", start: 0, end: 0 }; const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); @@ -355,3 +359,70 @@ test("a descending loop from an array's last index takes an integer counter for range: { min: 0, max: 4294967294 }, }); }); + +test("paired counters closing in on each other run as integers", () => { + const vars = new Map(locals); + for (const id of ["lo", "hi", "head"]) vars.set(id, { id, name: id, type: F64, mutable: true }); + const step = (id: string, op: "+" | "-", value = 1): IrStmt => ({ + kind: "assign", + localId: id, + value: { kind: "bin", op, left: ref(id), right: number(value), type: F64, loc }, + loc, + }); + const element = (index: IrExpr): IrExpr => ({ + kind: "arrayGet", + arr: { kind: "varRef", localId: "a", type: arrayOf(F64), loc }, + index, + type: F64, + loc, + }); + const paired = ( + lowStart: IrExpr, + highStart: IrExpr, + update = [step("lo", "+"), step("hi", "-")], + ) => + ({ + kind: "for", + loc, + init: { + kind: "block", + body: [ + { kind: "varDecl", localId: "lo", init: lowStart, loc }, + { kind: "varDecl", localId: "hi", init: highStart, loc }, + ], + loc, + }, + cond: { kind: "bin", op: "<", left: ref("lo"), right: ref("hi"), type: BOOL, loc }, + update: { kind: "block", body: update, loc }, + body: [{ kind: "exprStmt", expr: element(ref("hi")), loc }], + }) as IrStmt & { kind: "for" }; + expect(matchIntegerPairedForLoop(paired(number(0), number(9)), vars)).toMatchObject({ + low: { localId: "lo", step: 1, startGuarded: false }, + high: { localId: "hi", step: -1, startGuarded: false }, + guarded: false, + range: { min: 0, max: 9 }, + }); + // A start without a known range is tested at run time. + expect(matchIntegerPairedForLoop(paired(number(0), ref("head")), vars)).toMatchObject({ + high: { startGuarded: true }, + guarded: true, + }); + // Counters moving the same way, written by the body, captured, started + // from each other, or never used as an index are refused. + expect( + matchIntegerPairedForLoop( + paired(number(0), number(9), [step("lo", "+"), step("hi", "+")]), + vars, + ), + ).toBeNull(); + const writing = paired(number(0), number(9)); + writing.body.push(step("hi", "-", 2)); + expect(matchIntegerPairedForLoop(writing, vars)).toBeNull(); + const boxed = new Map(vars); + boxed.set("hi", { ...vars.get("hi")!, boxed: true }); + expect(matchIntegerPairedForLoop(paired(number(0), number(9)), boxed)).toBeNull(); + expect(matchIntegerPairedForLoop(paired(number(0), ref("lo")), vars)).toBeNull(); + const unused = paired(number(0), number(9)); + unused.body = []; + expect(matchIntegerPairedForLoop(unused, vars)).toBeNull(); +}); diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index 9b46756856..e80680d6e1 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -281,6 +281,120 @@ export function matchIntegerCountedForLoop( }; } +/** A for loop whose two counters close in on each other: + * + * for (let lo = a, hi = b; lo < hi; lo++, hi--) { ... } + * + * The first counter only grows and the second only shrinks, by positive + * literal steps, the body writes neither, and the loop runs while the first + * is below (or at most) the second. Every body entry therefore holds both + * between the first start and the second start: exact integers when the + * starts are. A start without a proven integer range is tested at run time + * on a versioned loop, as a counted loop's is. */ +export interface IntegerPairedCounter { + localId: string; + start: IrExpr; + /** The signed step of each update. */ + step: number; + startGuarded: boolean; +} +export interface IntegerPairedForLoop { + low: IntegerPairedCounter; + high: IntegerPairedCounter; + inclusive: boolean; + guarded: boolean; + /** Both counters' values on every body entry. */ + range: IntegerRange; +} + +export function matchIntegerPairedForLoop( + stmt: IrStmt & { kind: "for" }, + locals: ReadonlyMap, + ranges: IntegerRanges = new Map(), +): IntegerPairedForLoop | null { + const init = stmt.init, + update = stmt.update, + cond = stmt.cond; + if (init?.kind !== "block" || init.body.length !== 2) return null; + if (update?.kind !== "block" || update.body.length !== 2) return null; + if (cond?.kind !== "bin" || (cond.op !== "<" && cond.op !== "<=")) return null; + if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null; + const lowId = cond.left.localId, + highId = cond.right.localId; + if (lowId === highId) return null; + const counter = (id: string): IntegerPairedCounter | null => { + const decl = init.body.find((d) => d.kind === "varDecl" && d.localId === id); + if (decl?.kind !== "varDecl" || !decl.init) return null; + const local = locals.get(id); + if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null; + const start = decl.init; + // Each start is evaluated once in the head and again on a fallback path. + if ( + !stableLoopValue(start, locals, stmt.body, false, lowId) || + !stableLoopValue(start, locals, stmt.body, false, highId) + ) + return null; + const steps = update.body.map((u) => constantStep(u, id)).filter((v) => v !== null); + if (steps.length !== 1 || writesLocal(stmt.body, id)) return null; + const known = + start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0) + ? { min: start.value, max: start.value } + : ranges.get(start); + const startGuarded = !known && start.kind !== "numLit" && start.type.kind === "f64"; + if (!known && !startGuarded) return null; + if (known && (known.min < -Number.MAX_SAFE_INTEGER || known.max > Number.MAX_SAFE_INTEGER)) + return null; + return { localId: id, start, step: steps[0]!, startGuarded }; + }; + const low = counter(lowId), + high = counter(highId); + if (!low || !high || low.step <= 0 || high.step >= 0) return null; + const startRange = (c: IntegerPairedCounter): IntegerRange => + c.startGuarded + ? { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER } + : c.start.kind === "numLit" + ? { min: c.start.value, max: c.start.value } + : ranges.get(c.start)!; + const guarded = low.startGuarded || high.startGuarded; + // Only a loop whose counters index something gains from integer storage. + let useful = false, + cost = 0; + const isCounter = (e: IrExpr): boolean => + (e.kind === "varRef" && (e.localId === lowId || e.localId === highId)) || + (e.kind === "bin" && + (e.op === "+" || e.op === "-") && + (isCounter(e.left) || isCounter(e.right))); + const expr = (e: IrExpr): boolean => { + if (e.kind === "arrayGet" && isCounter(e.index)) useful = true; + if (e.kind === "strIntrinsic" && e.method === "charCodeAt" && e.args[0] && isCounter(e.args[0])) + useful = true; + if (e.kind === "bytesIntrinsic" && e.method === "get" && e.args[0] && isCounter(e.args[0])) + useful = true; + return (!guarded || ++cost <= 80) && everyExprChild(e, expr, bodyStmt); + }; + const bodyStmt = (s: IrStmt): boolean => { + if ((s.kind === "arraySet" || s.kind === "bytesSet") && isCounter(s.index)) useful = true; + if ( + guarded && + (s.kind === "for" || s.kind === "forOf" || s.kind === "while" || s.kind === "doWhile") + ) + return false; + return (!guarded || ++cost <= 80) && everyStmtChild(s, expr, bodyStmt); + }; + if (!stmt.body.every(bodyStmt) || !useful) return null; + return { + low, + high, + inclusive: cond.op === "<=", + guarded, + // A loop whose starts already fail the condition never enters the body. + range: { + min: startRange(low).min, + max: Math.max(startRange(low).min, startRange(high).max), + }, + }; +} + /** True when a lowered subtree writes `localId`. Local ids are unique per * function, so typed traversal includes writes * nested in expressions, branches, nested loops, and try/finally bodies. */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json new file mode 100644 index 0000000000..122d64bb68 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7140-paired-loop-counters.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7140-paired-loop-counters.ts b/tests/corpus/7140-paired-loop-counters.ts new file mode 100644 index 0000000000..ec1610d1a9 --- /dev/null +++ b/tests/corpus/7140-paired-loop-counters.ts @@ -0,0 +1,81 @@ +// For loops with two counters closing in on each other (`lo++, hi--`): +// in-place reversals, inclusive bounds, larger steps, early exits, starts +// that are fractions, negative zero, NaN, huge or already crossed, nested +// reversals, and counters a closure captures or the body writes. +function reverse(a: number[], from: number, to: number): number[] { + for (let lo = from, hi = to; lo < hi; lo++, hi--) { + const t = a[lo]!; + a[lo] = a[hi]!; + a[hi] = t; + } + return a; +} +const base = (): number[] => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; +console.log(reverse(base(), 0, 9).join(",")); +console.log(reverse(base(), 2, 6).join(",")); +console.log(reverse(base(), 6, 2).join(",")); +console.log(reverse(base(), 3, 3).join(",")); +// Unproven starts: fractions, negative zero, NaN and huge values keep JS +// semantics (charCodeAt truncates its index; out-of-range reads are NaN). +function codes(s: string, from: number, to: number): string { + const out: number[] = []; + for (let lo = from, hi = to; lo < hi; lo++, hi--) out.push(s.charCodeAt(lo), s.charCodeAt(hi), lo, hi); + return out.join(","); +} +const word = "abcdefgh"; +console.log(codes(word, 0.5, 4.5)); +console.log(codes(word, -0, 3), Object.is(-0 + 0, 0)); +console.log(codes(word, NaN, 4), "|", codes(word, 0, NaN)); +console.log(codes(word, 2 ** 53 - 3, 2 ** 53)); +console.log(codes(word, -3, 2)); + +function pairs(n: number): string { + const out: string[] = []; + for (let i = 0, j = n; i <= j; i += 2, j -= 3) out.push(`${i}:${j}`); + return out.join(" "); +} +console.log(pairs(20), "|", pairs(0), "|", pairs(-1)); + +function firstGap(a: number[]): number { + let found = -1; + for (let lo = 0, hi = a.length - 1; lo < hi; lo++, hi--) { + if (a[lo] === a[hi]) continue; + found = lo; + break; + } + return found; +} +console.log(firstGap([1, 2, 3, 2, 1]), firstGap([1, 2, 3, 4, 2, 1]), firstGap([])); + +// The flip loop of a permutation puzzle: the next index comes from the array. +function flips(perm: number[]): number { + const work = perm.slice(); + let count = 0; + let head = work[0]!; + while (head !== 0) { + for (let lo = 0, hi = head; lo < hi; lo++, hi--) { + const t = work[lo]!; + work[lo] = work[hi]!; + work[hi] = t; + } + count++; + head = work[0]!; + } + return count; +} +console.log(flips([3, 1, 0, 2]), flips([4, 3, 2, 1, 0]), flips([0, 1, 2])); + +// Counters a closure reads, or the body writes, stay ordinary bindings. +const readers: (() => number)[] = []; +for (let lo = 0, hi = 4; lo < hi; lo++, hi--) readers.push(() => lo * 10 + hi); +console.log(readers.map((f) => f()).join(",")); +const seen: number[] = []; +for (let lo = 0, hi = 10; lo < hi; lo++, hi--) { + seen.push(lo, hi); + if (lo === 2) hi -= 3; +} +console.log(seen.join(",")); +const text = "abcdefgh"; +let mirrored = ""; +for (let lo = 0, hi = text.length - 1; lo <= hi; lo++, hi--) mirrored += text.charAt(hi) + text.charCodeAt(lo); +console.log(mirrored); diff --git a/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json b/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json new file mode 100644 index 0000000000..549fd6a4ae --- /dev/null +++ b/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json @@ -0,0 +1 @@ +{ "plain": 3880, "san": 6000 } From 054cb50ea4381566b4438a0e5b44448b426e4c04 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:31:20 -0500 Subject: [PATCH 078/395] 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 8bb8bc3aab..80e0b00447 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 d31f14d12b..d040e8c93f 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 8284d40aba..ca61a8e495 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 7102f37ab1..c6e3d0c1e1 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 4be5d0b7ef..14c67f70a5 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 fa78765791..054b16201e 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 88ccbbc339..bdfe75f2e1 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 0000000000..5fff9651ac --- /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 0000000000..b2830e983d --- /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 0000000000..891c4ffe0d --- /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 c00d1b4f66..3d7da0c379 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 6c41b3dbd8..d08b94c148 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 02c4d828f2..8974fc7ce7 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 223517e925..7a1685a024 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 b8f16630b5..e8b75e5fad 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 ee357d0729..faba7dc90e 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 0000000000..5a1978cd9e --- /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 0000000000..93691a78f5 --- /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 0000000000..aa18cfca93 --- /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 0000000000..184d2ea9f8 --- /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 0000000000..b39c9d9734 --- /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 0000000000..dcd73aea74 --- /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 8af6998ca6..38749c168d 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 d1ff061833279865ddff6cba84b4f2d5dd362ebc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:48:13 -0500 Subject: [PATCH 079/395] Revert "Run two-counter for loops on integer counters" This reverts #1768 (8fec4484d), which made main CI fail. --- .../src/backend/llvm/counted-loops.ts | 81 +++++-------- .../src/backend/llvm/emit-statements.ts | 58 +-------- .../compiler/src/ir/integer-loops.test.ts | 73 +---------- packages/compiler/src/ir/integer-loops.ts | 114 ------------------ .../corpus/7140-paired-loop-counters.ts.json | 6 - tests/corpus/7140-paired-loop-counters.ts | 81 ------------- .../7140-paired-loop-counters.ts.json | 1 - 7 files changed, 37 insertions(+), 377 deletions(-) delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json delete mode 100644 tests/corpus/7140-paired-loop-counters.ts delete mode 100644 tests/harness/corpus-costs/7140-paired-loop-counters.ts.json diff --git a/packages/compiler/src/backend/llvm/counted-loops.ts b/packages/compiler/src/backend/llvm/counted-loops.ts index 02943b8523..36fc63e1ff 100644 --- a/packages/compiler/src/backend/llvm/counted-loops.ts +++ b/packages/compiler/src/backend/llvm/counted-loops.ts @@ -1,5 +1,4 @@ -import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; -import type { IrExpr } from "../../ir/ir.js"; +import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { f64Lit } from "./common.js"; @@ -60,51 +59,37 @@ export function emitCountedLoopSafety( `${safe} = fcmp ${loop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(loop.guardLimit)}`, ); } - if (loop.startGuarded) safe = andSafe(host, safe, emitExactStart(host, loop.start)); - return safe; -} - -/** The run-time test of a guarded paired loop: each unproven start is an - * exact safe integer. */ -export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPairedForLoop): string { - let safe = "true"; - for (const counter of [loop.low, loop.high]) - if (counter.startGuarded) safe = andSafe(host, safe, emitExactStart(host, counter.start)); + if (loop.startGuarded) { + // An exact integer of at most 2^53 - 1 in magnitude, and not -0 + // (the bit patterns of the value and its round trip agree). + // NaN and infinities fail the ordered range tests. + const start = host.emitExpr(loop.start); + const low = B.tmp(), + high = B.tmp(), + raw = B.tmp(), + wide = B.tmp(), + back = B.tmp(), + bits = B.tmp(), + backBits = B.tmp(), + exact = B.tmp(), + inRange = B.tmp(), + ok = B.tmp(); + B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`); + B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`); + B.line(`${inRange} = and i1 ${low}, ${high}`); + B.line(`${raw} = fptosi double ${start.name} to i64`); + B.line(`${wide} = freeze i64 ${raw}`); + B.line(`${back} = sitofp i64 ${wide} to double`); + B.line(`${bits} = bitcast double ${start.name} to i64`); + B.line(`${backBits} = bitcast double ${back} to i64`); + B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); + B.line(`${ok} = and i1 ${inRange}, ${exact}`); + if (safe === "true") safe = ok; + else { + const both = B.tmp(); + B.line(`${both} = and i1 ${safe}, ${ok}`); + safe = both; + } + } return safe; } - -function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string { - if (safe === "true") return ok; - const both = host.B.tmp(); - host.B.line(`${both} = and i1 ${safe}, ${ok}`); - return both; -} - -/** An exact integer of at most 2^53 - 1 in magnitude, and not -0 (the bit - * patterns of the value and its round trip agree). NaN and infinities fail - * the ordered range tests. */ -function emitExactStart(host: LlvmEmitterContext, expr: IrExpr): string { - const B = host.B; - const start = host.emitExpr(expr); - const low = B.tmp(), - high = B.tmp(), - raw = B.tmp(), - wide = B.tmp(), - back = B.tmp(), - bits = B.tmp(), - backBits = B.tmp(), - exact = B.tmp(), - inRange = B.tmp(), - ok = B.tmp(); - B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`); - B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`); - B.line(`${inRange} = and i1 ${low}, ${high}`); - B.line(`${raw} = fptosi double ${start.name} to i64`); - B.line(`${wide} = freeze i64 ${raw}`); - B.line(`${back} = sitofp i64 ${wide} to double`); - B.line(`${bits} = bitcast double ${start.name} to i64`); - B.line(`${backBits} = bitcast double ${back} to i64`); - B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); - B.line(`${ok} = and i1 ${inRange}, ${exact}`); - return ok; -} diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index e15de353a8..d2e96994bf 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -42,15 +42,10 @@ import { matchIntegerArrayForLoop, matchIntegerBytesForLoop, matchIntegerCountedForLoop, - matchIntegerPairedForLoop, } from "../../ir/integer-loops.js"; import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js"; import { countedDenseShape, emitDenseLoopGuard, matchDenseArrayLoop } from "./dense-array-loops.js"; -import { - emitCountedLoopLimit, - emitCountedLoopSafety, - emitPairedLoopSafety, -} from "./counted-loops.js"; +import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js"; import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js"; import { mangleLocal } from "../mangle.js"; import { LlvmUnsupportedError } from "./unsupported.js"; @@ -871,12 +866,6 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.unaliasedArrays, ) : null; - // Two counters closing in on each other (`lo++, hi--`) take the same - // integer storage as a counted loop's one. - const pairedLoop = - !integerLoop && !countedLoop && emitter.countedLoopsEnabled - ? matchIntegerPairedForLoop(s, emitter.numericLocals, emitter.integerRanges) - : null; const window = countedLoop && emitter.byteWindowsEnabled ? matchByteWindow( @@ -1009,10 +998,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { break; } let countedJoin: string | null = null; - if ((countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) || pairedLoop?.guarded) { - const safe = countedLoop - ? emitCountedLoopSafety(emitter, countedLoop) - : emitPairedLoopSafety(emitter, pairedLoop!); + if (countedLoop?.guarded && !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"); @@ -1027,7 +1014,6 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.scopes.push([]); let integerSlot: string | null = null; let countedLimit: string | null = null; - const pairedSlots: string[] = []; if (integerLoop) { integerSlot = B.slot(); B.entryAllocas.push( @@ -1072,24 +1058,6 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { : countedLoop.range, }); countedLimit = emitCountedLoopLimit(emitter, countedLoop); - } else if (pairedLoop) { - for (const counter of [pairedLoop.low, pairedLoop.high]) { - const slot = B.slot(); - B.entryAllocas.push( - `${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(counter.localId)!.name}`, - ); - const value = emitter.emitExpr(counter.start); - const start = B.tmp(); - B.line(`${start} = fptosi double ${value.name} to i64`); - B.line(`store i64 ${start}, ptr ${slot}`); - pairedSlots.push(slot); - emitter.integerLoopBindings.set(counter.localId, { - slot, - type: "i64", - signed: true, - range: pairedLoop.range, - }); - } } else if (s.init) { // A multi-declarator head shares the loop's scope. Emitting its // IR block as an ordinary block would release captured/ref locals @@ -1171,14 +1139,6 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { `${inBounds} = icmp ${countedLoop.step > 0 ? "slt" : "sgt"} i64 ${index}, ${countedLimit}`, ); B.condBr(inBounds, lb, exit); - } else if (pairedLoop) { - const low = B.tmp(), - high = B.tmp(), - open = B.tmp(); - B.line(`${low} = load i64, ptr ${pairedSlots[0]}`); - B.line(`${high} = load i64, ptr ${pairedSlots[1]}`); - B.line(`${open} = icmp ${pairedLoop.inclusive ? "sle" : "slt"} i64 ${low}, ${high}`); - B.condBr(open, lb, exit); } else if (s.cond) B.condBr(emitter.emitCondition(s.cond), lb, exit); else B.br(lb); B.startBlock(lb); @@ -1210,14 +1170,6 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.line(`${old} = load i64, ptr ${integerSlot}`); B.line(`${next} = add nsw i64 ${old}, ${countedLoop.step}`); B.line(`store i64 ${next}, ptr ${integerSlot}`); - } else if (pairedLoop) { - [pairedLoop.low, pairedLoop.high].forEach((counter, i) => { - const old = B.tmp(); - const next = B.tmp(); - B.line(`${old} = load i64, ptr ${pairedSlots[i]}`); - B.line(`${next} = add nsw i64 ${old}, ${counter.step}`); - B.line(`store i64 ${next}, ptr ${pairedSlots[i]}`); - }); } else if (s.update) emitter.emitStmt(s.update); B.br(lc); } @@ -1226,10 +1178,6 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.releaseScope(emitter.scopes.pop()!); if (integerLoop) emitter.integerLoopBindings.delete(integerLoop.localId); if (countedLoop) emitter.integerLoopBindings.delete(countedLoop.localId); - if (pairedLoop) { - emitter.integerLoopBindings.delete(pairedLoop.low.localId); - emitter.integerLoopBindings.delete(pairedLoop.high.localId); - } if (integerArrayLoop) emitter.integerArrayBindings.delete(integerArrayLoop.localId); if (countedJoin) { B.br(countedJoin); diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index 24bbf5be0f..aada3b0535 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -1,10 +1,6 @@ import { expect, test } from "vitest"; import { BOOL, F64, STRING, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "./ir.js"; -import { - matchIntegerArrayForLoop, - matchIntegerCountedForLoop, - matchIntegerPairedForLoop, -} from "./integer-loops.js"; +import { matchIntegerArrayForLoop, matchIntegerCountedForLoop } from "./integer-loops.js"; const loc = { file: "integer-array-loop.ts", start: 0, end: 0 }; const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); @@ -359,70 +355,3 @@ test("a descending loop from an array's last index takes an integer counter for range: { min: 0, max: 4294967294 }, }); }); - -test("paired counters closing in on each other run as integers", () => { - const vars = new Map(locals); - for (const id of ["lo", "hi", "head"]) vars.set(id, { id, name: id, type: F64, mutable: true }); - const step = (id: string, op: "+" | "-", value = 1): IrStmt => ({ - kind: "assign", - localId: id, - value: { kind: "bin", op, left: ref(id), right: number(value), type: F64, loc }, - loc, - }); - const element = (index: IrExpr): IrExpr => ({ - kind: "arrayGet", - arr: { kind: "varRef", localId: "a", type: arrayOf(F64), loc }, - index, - type: F64, - loc, - }); - const paired = ( - lowStart: IrExpr, - highStart: IrExpr, - update = [step("lo", "+"), step("hi", "-")], - ) => - ({ - kind: "for", - loc, - init: { - kind: "block", - body: [ - { kind: "varDecl", localId: "lo", init: lowStart, loc }, - { kind: "varDecl", localId: "hi", init: highStart, loc }, - ], - loc, - }, - cond: { kind: "bin", op: "<", left: ref("lo"), right: ref("hi"), type: BOOL, loc }, - update: { kind: "block", body: update, loc }, - body: [{ kind: "exprStmt", expr: element(ref("hi")), loc }], - }) as IrStmt & { kind: "for" }; - expect(matchIntegerPairedForLoop(paired(number(0), number(9)), vars)).toMatchObject({ - low: { localId: "lo", step: 1, startGuarded: false }, - high: { localId: "hi", step: -1, startGuarded: false }, - guarded: false, - range: { min: 0, max: 9 }, - }); - // A start without a known range is tested at run time. - expect(matchIntegerPairedForLoop(paired(number(0), ref("head")), vars)).toMatchObject({ - high: { startGuarded: true }, - guarded: true, - }); - // Counters moving the same way, written by the body, captured, started - // from each other, or never used as an index are refused. - expect( - matchIntegerPairedForLoop( - paired(number(0), number(9), [step("lo", "+"), step("hi", "+")]), - vars, - ), - ).toBeNull(); - const writing = paired(number(0), number(9)); - writing.body.push(step("hi", "-", 2)); - expect(matchIntegerPairedForLoop(writing, vars)).toBeNull(); - const boxed = new Map(vars); - boxed.set("hi", { ...vars.get("hi")!, boxed: true }); - expect(matchIntegerPairedForLoop(paired(number(0), number(9)), boxed)).toBeNull(); - expect(matchIntegerPairedForLoop(paired(number(0), ref("lo")), vars)).toBeNull(); - const unused = paired(number(0), number(9)); - unused.body = []; - expect(matchIntegerPairedForLoop(unused, vars)).toBeNull(); -}); diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index e80680d6e1..9b46756856 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -281,120 +281,6 @@ export function matchIntegerCountedForLoop( }; } -/** A for loop whose two counters close in on each other: - * - * for (let lo = a, hi = b; lo < hi; lo++, hi--) { ... } - * - * The first counter only grows and the second only shrinks, by positive - * literal steps, the body writes neither, and the loop runs while the first - * is below (or at most) the second. Every body entry therefore holds both - * between the first start and the second start: exact integers when the - * starts are. A start without a proven integer range is tested at run time - * on a versioned loop, as a counted loop's is. */ -export interface IntegerPairedCounter { - localId: string; - start: IrExpr; - /** The signed step of each update. */ - step: number; - startGuarded: boolean; -} -export interface IntegerPairedForLoop { - low: IntegerPairedCounter; - high: IntegerPairedCounter; - inclusive: boolean; - guarded: boolean; - /** Both counters' values on every body entry. */ - range: IntegerRange; -} - -export function matchIntegerPairedForLoop( - stmt: IrStmt & { kind: "for" }, - locals: ReadonlyMap, - ranges: IntegerRanges = new Map(), -): IntegerPairedForLoop | null { - const init = stmt.init, - update = stmt.update, - cond = stmt.cond; - if (init?.kind !== "block" || init.body.length !== 2) return null; - if (update?.kind !== "block" || update.body.length !== 2) return null; - if (cond?.kind !== "bin" || (cond.op !== "<" && cond.op !== "<=")) return null; - if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null; - const lowId = cond.left.localId, - highId = cond.right.localId; - if (lowId === highId) return null; - const counter = (id: string): IntegerPairedCounter | null => { - const decl = init.body.find((d) => d.kind === "varDecl" && d.localId === id); - if (decl?.kind !== "varDecl" || !decl.init) return null; - const local = locals.get(id); - if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null; - const start = decl.init; - // Each start is evaluated once in the head and again on a fallback path. - if ( - !stableLoopValue(start, locals, stmt.body, false, lowId) || - !stableLoopValue(start, locals, stmt.body, false, highId) - ) - return null; - const steps = update.body.map((u) => constantStep(u, id)).filter((v) => v !== null); - if (steps.length !== 1 || writesLocal(stmt.body, id)) return null; - const known = - start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0) - ? { min: start.value, max: start.value } - : ranges.get(start); - const startGuarded = !known && start.kind !== "numLit" && start.type.kind === "f64"; - if (!known && !startGuarded) return null; - if (known && (known.min < -Number.MAX_SAFE_INTEGER || known.max > Number.MAX_SAFE_INTEGER)) - return null; - return { localId: id, start, step: steps[0]!, startGuarded }; - }; - const low = counter(lowId), - high = counter(highId); - if (!low || !high || low.step <= 0 || high.step >= 0) return null; - const startRange = (c: IntegerPairedCounter): IntegerRange => - c.startGuarded - ? { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER } - : c.start.kind === "numLit" - ? { min: c.start.value, max: c.start.value } - : ranges.get(c.start)!; - const guarded = low.startGuarded || high.startGuarded; - // Only a loop whose counters index something gains from integer storage. - let useful = false, - cost = 0; - const isCounter = (e: IrExpr): boolean => - (e.kind === "varRef" && (e.localId === lowId || e.localId === highId)) || - (e.kind === "bin" && - (e.op === "+" || e.op === "-") && - (isCounter(e.left) || isCounter(e.right))); - const expr = (e: IrExpr): boolean => { - if (e.kind === "arrayGet" && isCounter(e.index)) useful = true; - if (e.kind === "strIntrinsic" && e.method === "charCodeAt" && e.args[0] && isCounter(e.args[0])) - useful = true; - if (e.kind === "bytesIntrinsic" && e.method === "get" && e.args[0] && isCounter(e.args[0])) - useful = true; - return (!guarded || ++cost <= 80) && everyExprChild(e, expr, bodyStmt); - }; - const bodyStmt = (s: IrStmt): boolean => { - if ((s.kind === "arraySet" || s.kind === "bytesSet") && isCounter(s.index)) useful = true; - if ( - guarded && - (s.kind === "for" || s.kind === "forOf" || s.kind === "while" || s.kind === "doWhile") - ) - return false; - return (!guarded || ++cost <= 80) && everyStmtChild(s, expr, bodyStmt); - }; - if (!stmt.body.every(bodyStmt) || !useful) return null; - return { - low, - high, - inclusive: cond.op === "<=", - guarded, - // A loop whose starts already fail the condition never enters the body. - range: { - min: startRange(low).min, - max: Math.max(startRange(low).min, startRange(high).max), - }, - }; -} - /** True when a lowered subtree writes `localId`. Local ids are unique per * function, so typed traversal includes writes * nested in expressions, branches, nested loops, and try/finally bodies. */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json deleted file mode 100644 index 122d64bb68..0000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/7140-paired-loop-counters.ts" - ], - "diags": [] -} diff --git a/tests/corpus/7140-paired-loop-counters.ts b/tests/corpus/7140-paired-loop-counters.ts deleted file mode 100644 index ec1610d1a9..0000000000 --- a/tests/corpus/7140-paired-loop-counters.ts +++ /dev/null @@ -1,81 +0,0 @@ -// For loops with two counters closing in on each other (`lo++, hi--`): -// in-place reversals, inclusive bounds, larger steps, early exits, starts -// that are fractions, negative zero, NaN, huge or already crossed, nested -// reversals, and counters a closure captures or the body writes. -function reverse(a: number[], from: number, to: number): number[] { - for (let lo = from, hi = to; lo < hi; lo++, hi--) { - const t = a[lo]!; - a[lo] = a[hi]!; - a[hi] = t; - } - return a; -} -const base = (): number[] => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; -console.log(reverse(base(), 0, 9).join(",")); -console.log(reverse(base(), 2, 6).join(",")); -console.log(reverse(base(), 6, 2).join(",")); -console.log(reverse(base(), 3, 3).join(",")); -// Unproven starts: fractions, negative zero, NaN and huge values keep JS -// semantics (charCodeAt truncates its index; out-of-range reads are NaN). -function codes(s: string, from: number, to: number): string { - const out: number[] = []; - for (let lo = from, hi = to; lo < hi; lo++, hi--) out.push(s.charCodeAt(lo), s.charCodeAt(hi), lo, hi); - return out.join(","); -} -const word = "abcdefgh"; -console.log(codes(word, 0.5, 4.5)); -console.log(codes(word, -0, 3), Object.is(-0 + 0, 0)); -console.log(codes(word, NaN, 4), "|", codes(word, 0, NaN)); -console.log(codes(word, 2 ** 53 - 3, 2 ** 53)); -console.log(codes(word, -3, 2)); - -function pairs(n: number): string { - const out: string[] = []; - for (let i = 0, j = n; i <= j; i += 2, j -= 3) out.push(`${i}:${j}`); - return out.join(" "); -} -console.log(pairs(20), "|", pairs(0), "|", pairs(-1)); - -function firstGap(a: number[]): number { - let found = -1; - for (let lo = 0, hi = a.length - 1; lo < hi; lo++, hi--) { - if (a[lo] === a[hi]) continue; - found = lo; - break; - } - return found; -} -console.log(firstGap([1, 2, 3, 2, 1]), firstGap([1, 2, 3, 4, 2, 1]), firstGap([])); - -// The flip loop of a permutation puzzle: the next index comes from the array. -function flips(perm: number[]): number { - const work = perm.slice(); - let count = 0; - let head = work[0]!; - while (head !== 0) { - for (let lo = 0, hi = head; lo < hi; lo++, hi--) { - const t = work[lo]!; - work[lo] = work[hi]!; - work[hi] = t; - } - count++; - head = work[0]!; - } - return count; -} -console.log(flips([3, 1, 0, 2]), flips([4, 3, 2, 1, 0]), flips([0, 1, 2])); - -// Counters a closure reads, or the body writes, stay ordinary bindings. -const readers: (() => number)[] = []; -for (let lo = 0, hi = 4; lo < hi; lo++, hi--) readers.push(() => lo * 10 + hi); -console.log(readers.map((f) => f()).join(",")); -const seen: number[] = []; -for (let lo = 0, hi = 10; lo < hi; lo++, hi--) { - seen.push(lo, hi); - if (lo === 2) hi -= 3; -} -console.log(seen.join(",")); -const text = "abcdefgh"; -let mirrored = ""; -for (let lo = 0, hi = text.length - 1; lo <= hi; lo++, hi--) mirrored += text.charAt(hi) + text.charCodeAt(lo); -console.log(mirrored); diff --git a/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json b/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json deleted file mode 100644 index 549fd6a4ae..0000000000 --- a/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json +++ /dev/null @@ -1 +0,0 @@ -{ "plain": 3880, "san": 6000 } From 1a240fbf107766e102c7c027b02ea5f78b62d6d7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:49:33 -0500 Subject: [PATCH 080/395] Support asymmetric keys, signatures and Ed25519 in node:crypto and WebCrypto Co-authored-by: saboursepideh <25549001+saboursepideh@users.noreply.github.com> --- .../limitations/builtin-apis/crypto.mdx | 16 +- .../src/generated/node-v24-compatibility.json | 108 +- .../generated/node-v24-internal.json | 226 +- internal/compatibility/static-support.json | 3 +- .../ambient/scriptc-node-fallback.d.ts | 249 +- .../src/backend/executable-features.ts | 2 + packages/compiler/src/backend/library-pack.ts | 2 + .../compiler/src/backend/llvm/lib-shared.ts | 12 + .../src/backend/native-link-info-core.ts | 6 + .../compiler/src/backend/native/contracts.ts | 6 + .../compiler/src/backend/native/executable.ts | 8 +- .../src/backend/runtime-pack-targets.test.ts | 2 + .../compiler/src/backend/runtime-pack.test.ts | 2 + .../src/backend/target-diagnostics.ts | 21 +- packages/compiler/src/backend/warm-cache.ts | 2 + .../src/executable/executable-cache.test.ts | 2 + .../src/executable/executable-cache.ts | 4 + .../src/frontend/lowering/builtins/crypto.ts | 54 + .../frontend/lowering/builtins/key-object.ts | 31 +- .../frontend/lowering/builtins/webcrypto.ts | 31 +- .../src/frontend/lowering/surfaces.ts | 35 +- packages/compiler/src/frontend/type-mapper.ts | 102 +- packages/compiler/src/index.ts | 2 + packages/compiler/src/ir/builtin-effects.ts | 8 + .../compiler/src/ir/builtin-signatures.ts | 15 + packages/compiler/src/ir/runtime-features.ts | 65 + packages/compiler/src/library/ir-policy.ts | 8 + packages/compiler/src/native/cli.ts | 2 + packages/compiler/src/native/prepare.ts | 2 + .../compiler/src/native/sanitizer.test.ts | 2 + packages/compiler/surface-manifest.json | 50 +- .../tests/corpus/7080-crypto-ed25519.mjs.json | 6 + .../corpus/7081-webcrypto-ed25519.mjs.json | 6 + .../corpus/7082-crypto-key-objects.ts.json | 6 + .../corpus/7083-crypto-sign-verify.ts.json | 6 + .../corpus/7084-crypto-key-pairs.cjs.json | 6 + .../corpus/7085-webcrypto-ecdsa-rsa.ts.json | 6 + .../7086-webcrypto-ecdsa-rsa-errors.mjs.json | 6 + .../tests/fixtures/node-types/keys.ts.json | 6 + .../node-types/webcrypto-ecdsa.ts.json | 6 + .../runtime-pack-matrix.mjs | 2 +- .../runtime-darwin-arm64/scripts/build.mjs | 3 +- .../runtime-pack-matrix.mjs | 5 +- .../runtime-pack-common/scripts/build.mjs | 3 +- .../runtime-wasm32-wasi/scripts/build.mjs | 3 + packages/runtime/src/scr_crypto.h | 18 + packages/runtime/src/scr_dyn.h | 5 + packages/runtime/src/scr_ed25519.c | 76 + packages/runtime/src/scr_lib.c | 12 +- packages/runtime/src/scr_pki.c | 2118 ++++++++++++ packages/runtime/src/scr_pki.h | 75 + packages/runtime/src/scr_webcrypto.c | 164 +- packages/runtime/src/scr_webcrypto.h | 58 + packages/runtime/src/scr_webcrypto_pki.c | 845 +++++ packages/runtime/vendor/README.md | 12 + packages/runtime/vendor/libsodium/LICENSE | 18 + packages/runtime/vendor/libsodium/README.md | 7 + packages/runtime/vendor/libsodium/common.h | 285 ++ .../vendor/libsodium/crypto_hash_sha512.h | 60 + .../libsodium/crypto_scalarmult_curve25519.h | 42 + .../vendor/libsodium/crypto_sign_ed25519.h | 124 + .../vendor/libsodium/crypto_verify_16.h | 23 + .../vendor/libsodium/crypto_verify_32.h | 23 + .../vendor/libsodium/crypto_verify_64.h | 23 + .../runtime/vendor/libsodium/ed25519_ref10.c | 2858 +++++++++++++++++ .../runtime/vendor/libsodium/ed25519_ref10.h | 142 + .../vendor/libsodium/ed25519_ref10_fe_25_5.h | 1030 ++++++ packages/runtime/vendor/libsodium/export.h | 57 + .../runtime/vendor/libsodium/fe_25_5/base.h | 1344 ++++++++ .../runtime/vendor/libsodium/fe_25_5/base2.h | 40 + .../vendor/libsodium/fe_25_5/constants.h | 40 + .../runtime/vendor/libsodium/fe_25_5/fe.h | 220 ++ .../runtime/vendor/libsodium/hash_sha512_cp.c | 284 ++ packages/runtime/vendor/libsodium/keypair.c | 84 + packages/runtime/vendor/libsodium/open.c | 98 + .../runtime/vendor/libsodium/randombytes.h | 72 + packages/runtime/vendor/libsodium/sign.c | 128 + .../vendor/libsodium/sign_ed25519_ref10.h | 18 + packages/runtime/vendor/libsodium/utils.h | 179 ++ packages/runtime/vendor/libsodium/verify.c | 95 + tests/corpus/7080-crypto-ed25519.mjs | 34 + tests/corpus/7081-webcrypto-ed25519.mjs | 39 + tests/corpus/7082-crypto-key-objects.ts | 132 + tests/corpus/7083-crypto-sign-verify.ts | 85 + tests/corpus/7084-crypto-key-pairs.cjs | 64 + tests/corpus/7085-webcrypto-ecdsa-rsa.ts | 66 + .../7086-webcrypto-ecdsa-rsa-errors.mjs | 58 + tests/diagnostics/webcrypto-refusals.ts | 16 +- tests/fixtures/node-types/fenced.ts | 16 +- tests/fixtures/node-types/keys.ts | 43 + tests/fixtures/node-types/webcrypto-ecdsa.ts | 34 + .../__snapshots__/node-types-fenced.txt | 16 +- .../__snapshots__/webcrypto-refusals.ts.txt | 33 +- .../corpus-costs/7080-crypto-ed25519.mjs.json | 1 + .../7081-webcrypto-ed25519.mjs.json | 1 + .../7082-crypto-key-objects.ts.json | 1 + .../7083-crypto-sign-verify.ts.json | 1 + .../7084-crypto-key-pairs.cjs.json | 1 + .../7085-webcrypto-ecdsa-rsa.ts.json | 1 + .../7086-webcrypto-ecdsa-rsa-errors.mjs.json | 1 + tests/harness/errors.test.ts | 71 +- tests/harness/library-asyncfree.test.ts | 9 + tests/harness/project-config.test.ts | 32 + .../self-hosting-native-toolchain.test.ts | 2 + .../harness/self-hosting-runtime-pack.test.ts | 2 + 105 files changed, 12304 insertions(+), 280 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7080-crypto-ed25519.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7081-webcrypto-ed25519.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7082-crypto-key-objects.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7083-crypto-sign-verify.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7084-crypto-key-pairs.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7085-webcrypto-ecdsa-rsa.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/keys.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-ecdsa.ts.json create mode 100644 packages/runtime/src/scr_ed25519.c create mode 100644 packages/runtime/src/scr_pki.c create mode 100644 packages/runtime/src/scr_pki.h create mode 100644 packages/runtime/src/scr_webcrypto.h create mode 100644 packages/runtime/src/scr_webcrypto_pki.c create mode 100644 packages/runtime/vendor/libsodium/LICENSE create mode 100644 packages/runtime/vendor/libsodium/README.md create mode 100644 packages/runtime/vendor/libsodium/common.h create mode 100644 packages/runtime/vendor/libsodium/crypto_hash_sha512.h create mode 100644 packages/runtime/vendor/libsodium/crypto_scalarmult_curve25519.h create mode 100644 packages/runtime/vendor/libsodium/crypto_sign_ed25519.h create mode 100644 packages/runtime/vendor/libsodium/crypto_verify_16.h create mode 100644 packages/runtime/vendor/libsodium/crypto_verify_32.h create mode 100644 packages/runtime/vendor/libsodium/crypto_verify_64.h create mode 100644 packages/runtime/vendor/libsodium/ed25519_ref10.c create mode 100644 packages/runtime/vendor/libsodium/ed25519_ref10.h create mode 100644 packages/runtime/vendor/libsodium/ed25519_ref10_fe_25_5.h create mode 100644 packages/runtime/vendor/libsodium/export.h create mode 100644 packages/runtime/vendor/libsodium/fe_25_5/base.h create mode 100644 packages/runtime/vendor/libsodium/fe_25_5/base2.h create mode 100644 packages/runtime/vendor/libsodium/fe_25_5/constants.h create mode 100644 packages/runtime/vendor/libsodium/fe_25_5/fe.h create mode 100644 packages/runtime/vendor/libsodium/hash_sha512_cp.c create mode 100644 packages/runtime/vendor/libsodium/keypair.c create mode 100644 packages/runtime/vendor/libsodium/open.c create mode 100644 packages/runtime/vendor/libsodium/randombytes.h create mode 100644 packages/runtime/vendor/libsodium/sign.c create mode 100644 packages/runtime/vendor/libsodium/sign_ed25519_ref10.h create mode 100644 packages/runtime/vendor/libsodium/utils.h create mode 100644 packages/runtime/vendor/libsodium/verify.c create mode 100644 tests/corpus/7080-crypto-ed25519.mjs create mode 100644 tests/corpus/7081-webcrypto-ed25519.mjs create mode 100644 tests/corpus/7082-crypto-key-objects.ts create mode 100644 tests/corpus/7083-crypto-sign-verify.ts create mode 100644 tests/corpus/7084-crypto-key-pairs.cjs create mode 100644 tests/corpus/7085-webcrypto-ecdsa-rsa.ts create mode 100644 tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs create mode 100644 tests/fixtures/node-types/keys.ts create mode 100644 tests/fixtures/node-types/webcrypto-ecdsa.ts create mode 100644 tests/harness/corpus-costs/7080-crypto-ed25519.mjs.json create mode 100644 tests/harness/corpus-costs/7081-webcrypto-ed25519.mjs.json create mode 100644 tests/harness/corpus-costs/7082-crypto-key-objects.ts.json create mode 100644 tests/harness/corpus-costs/7083-crypto-sign-verify.ts.json create mode 100644 tests/harness/corpus-costs/7084-crypto-key-pairs.cjs.json create mode 100644 tests/harness/corpus-costs/7085-webcrypto-ecdsa-rsa.ts.json create mode 100644 tests/harness/corpus-costs/7086-webcrypto-ecdsa-rsa-errors.mjs.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 3c830c63d9..855968219b 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -4,7 +4,7 @@ `copy()` of a Hash whose stream flushed, and stream use of a value after its stream closed, throw a coded SC2020 error. Through `unknown` and `any` slots and untyped JavaScript, Hash and Hmac values keep their identity and support `update()`, `digest()`, and Hash's `copy()`, called or read as values; their stream and event members there throw a coded SC2020 error. `timingSafeEqual` compares any view or ArrayBuffer. - Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; asymmetric keys, `KeyObject.from()`, and `toCryptoKey()` are not available. + Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; `KeyObject.from()` and `toCryptoKey()` are not available. `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. @@ -15,5 +15,17 @@ `createECDH` and `ECDH.convertKey` support prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1, and brainpoolP512r1, on native targets, through `generateKeys()`, `computeSecret()`, `getPublicKey()`, `getPrivateKey()`, `setPrivateKey()`, and `setPublicKey()`, in compressed, uncompressed, and hybrid forms; the other curves `crypto.getCurves()` lists and wasm32-wasi builds are refused. A call on an ECDH value whose arguments no @types/node overload accepts (`getPublicKey("HEX")`) is refused at compile time, as is `computeSecret(view, encoding)`: Node reads that encoding as the input encoding and answers a Buffer where @types/node declares a string. ECDH property reads and writes throw a coded SC2020 error. - Other public-key operations remain unsupported. + 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. + They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`. + `crypto.sign`, `crypto.verify`, `createSign`, and `createVerify` support RSA PKCS#1 v1.5 and PSS padding and ECDSA with DER and IEEE P1363 encodings over SHA-1 and SHA-2 digests, and Ed25519 through `crypto.sign` and `crypto.verify` with a `null` or `undefined` algorithm. + Ed25519 signing, verification, and private keys link when a string literal in the program names `ed25519` (in any case); without one they throw a coded SC2020 error, while Ed25519 public keys still import and export. + Sign and Verify values accumulate `update()` data and are not writable streams. + Encrypted private keys (`passphrase`, `cipher`), the other key types (`rsa-pss`, `dsa`, `ed448`, `x25519`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused. + Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report. + Diffie-Hellman and `publicEncrypt`/`privateDecrypt` remain unsupported. + WebCrypto's `crypto.subtle` supports HMAC keys imported from `raw` with `sign` and `verify`, SHA-1 and SHA-2 `digest`, and `generateKey`, `importKey`, `exportKey`, `sign`, and `verify` for ECDSA (P-256, P-384, and P-521), Ed25519, RSASSA-PKCS1-v1_5, and RSA-PSS, with the `spki`, `pkcs8`, and `jwk` formats and `raw` for ECDSA and Ed25519 public keys. + The methods are called on `crypto.subtle` directly; other methods, algorithms, and SHA-3 digests throw a coded SC2020 error, and ECDSA, Ed25519, and RSA are refused in wasm32-wasi builds. + ECDSA, Ed25519, and RSA link when a string literal in the program names the algorithm; a name built only at run time throws a coded SC2020 error. Native derivations finish before their deferred callbacks, so computation blocks the caller. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 66b0ba3dd3..4898fe2187 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -11338,9 +11338,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -11361,9 +11361,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -11407,9 +11407,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -11430,9 +11430,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -11545,9 +11545,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -11568,9 +11568,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -12327,9 +12327,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -12373,9 +12373,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13523,9 +13523,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 by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13546,9 +13546,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 by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13684,9 +13684,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 by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13707,9 +13707,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 by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13730,9 +13730,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 by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13753,9 +13753,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 by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13776,9 +13776,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 by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -13799,9 +13799,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 by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -80162,9 +80162,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 documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80208,9 +80208,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 documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index c6a7a102c3..c886cccf9e 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19313,12 +19313,17 @@ "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/fixtures/node-types/cipher.ts", "tests/fixtures/node-types/ecdh.ts", + "tests/fixtures/node-types/keys.ts", "tests/harness/npm-static.test.ts" ] }, @@ -19961,8 +19966,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.createPrivateKey" + "status": "partial", + "evidence": "compiler-dedicated:crypto.createPrivateKey", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -20007,8 +20019,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.createPublicKey" + "status": "partial", + "evidence": "compiler-dedicated:crypto.createPublicKey", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -20057,8 +20076,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.createSign" + "status": "partial", + "evidence": "compiler-dedicated:crypto.createSign", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -20080,8 +20106,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.createVerify" + "status": "partial", + "evidence": "compiler-dedicated:crypto.createVerify", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -20199,8 +20232,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.generateKeyPair" + "status": "partial", + "evidence": "compiler-dedicated:crypto.generateKeyPair", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -20222,8 +20262,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.generateKeyPairSync" + "status": "partial", + "evidence": "compiler-dedicated:crypto.generateKeyPairSync", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -21096,8 +21143,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.sign" + "status": "partial", + "evidence": "compiler-dedicated:crypto.sign", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -21146,8 +21200,15 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.verify" + "status": "partial", + "evidence": "compiler-dedicated:crypto.verify", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -23007,8 +23068,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:asymmetricKeyDetails", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "ancestor" }, @@ -23030,8 +23098,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:asymmetricKeyType", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "ancestor" }, @@ -23168,8 +23243,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:Sign", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -23191,8 +23273,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:sign.sign", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -23214,8 +23303,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:sign.update", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -23237,8 +23333,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:Verify", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -23260,8 +23363,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:verify.update", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -23283,8 +23393,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:verify.verify", + "tests": [ + "tests/corpus/7080-crypto-ed25519.mjs", + "tests/corpus/7082-crypto-key-objects.ts", + "tests/corpus/7083-crypto-sign-verify.ts", + "tests/corpus/7084-crypto-key-pairs.cjs", + "tests/fixtures/node-types/keys.ts" + ] }, "anchorSource": "exact" }, @@ -106616,6 +106733,9 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -110543,6 +110663,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.digest", "tests": [ + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", @@ -110640,8 +110763,19 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.exportKey", + "tests": [ + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110686,8 +110820,19 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.generateKey", + "tests": [ + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110712,6 +110857,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.importKey", "tests": [ + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", @@ -110743,6 +110891,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.sign", "tests": [ + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", @@ -110797,6 +110948,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.verify", "tests": [ + "tests/corpus/7081-webcrypto-ed25519.mjs", + "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", + "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 23c7a159b6..d799d48873 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -194,7 +194,7 @@ "stdlib.abort": ["tests/harness/fetch-conformance.test.ts"] }, "features": [ - { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, + { "chapter": "webcrypto", "symbols": ["subtle.generateKey", "subtle.importKey", "subtle.exportKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7081-webcrypto-ed25519.mjs", "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, { "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, { "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] }, @@ -418,6 +418,7 @@ { "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] }, { "symbols": ["crypto.randomFill", "crypto.randomFillSync", "crypto.randomUUID", "crypto.randomUUIDv7"], "status": "supported", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/1358-crypto-random.ts"] }, { "symbols": ["crypto.createSecretKey", "crypto.generateKey", "crypto.generateKeySync", "KeyObject", "keyObject.export", "keyObject.equals", "keyObject.symmetricKeySize", "keyObject.type"], "status": "partial", "evidence": ["tests/corpus/5445-crypto-secret-key-objects.ts", "tests/corpus/5446-crypto-secret-key-errors.js"] }, + { "symbols": ["crypto.createPrivateKey", "crypto.createPublicKey", "crypto.generateKeyPair", "crypto.generateKeyPairSync", "crypto.sign", "crypto.verify", "crypto.createSign", "crypto.createVerify", "Sign", "sign.update", "sign.sign", "Verify", "verify.update", "verify.verify", "asymmetricKeyType", "asymmetricKeyDetails"], "status": "partial", "evidence": ["tests/corpus/7080-crypto-ed25519.mjs", "tests/corpus/7082-crypto-key-objects.ts", "tests/corpus/7083-crypto-sign-verify.ts", "tests/corpus/7084-crypto-key-pairs.cjs", "tests/fixtures/node-types/keys.ts"] }, { "symbols": ["crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] }, { "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] }, { "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index dbb3cd8fda..ca1913cd76 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -544,11 +544,12 @@ declare var DOMException: { readonly prototype: DOMException; }; -/* WebCrypto's HMAC slice (scr_webcrypto.c): crypto.subtle's importKey for - * raw HMAC keys, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as - * exactly that surface (the declarations mirror @types/node's webcrypto - * shapes). Every other SubtleCrypto method and crypto member is absent, - * so a use is an honest type error. CryptoKey values are native handles +/* WebCrypto (scr_webcrypto.c): crypto.subtle's importKey for raw HMAC + * keys, HMAC sign/verify, generateKey, importKey, exportKey, sign and + * verify for ECDSA, RSASSA-PKCS1-v1_5 and RSA-PSS, and the SHA-1/SHA-2 + * digests, typed as exactly that surface (the declarations mirror + * @types/node's webcrypto shapes). Every other SubtleCrypto method and + * crypto member is absent, so a use is an honest type error. CryptoKey values are native handles * whose four members read through the handle. */ declare var crypto: Crypto; /* The Crypto and SubtleCrypto constructors (feature probes only) stay @@ -623,27 +624,70 @@ interface JsonWebKey { x?: string; y?: string; } -/* The 'jwk' form type-checks like @types/node's; the compiler refuses it - * (the JsonWebKey import is not implemented). */ +interface EcKeyGenParams extends Algorithm { + namedCurve: string; +} +interface EcKeyImportParams extends Algorithm { + namedCurve: string; +} +interface EcdsaParams extends Algorithm { + hash: HashAlgorithmIdentifier; +} +interface RsaHashedKeyGenParams extends Algorithm { + modulusLength: number; + publicExponent: Uint8Array; + hash: HashAlgorithmIdentifier; +} +interface RsaHashedImportParams extends Algorithm { + hash: HashAlgorithmIdentifier; +} +interface RsaPssParams extends Algorithm { + saltLength: number; +} +interface EcKeyAlgorithm extends KeyAlgorithm { + namedCurve: string; +} +interface RsaHashedKeyAlgorithm extends KeyAlgorithm { + modulusLength: number; + publicExponent: Uint8Array; + hash: KeyAlgorithm; +} +interface CryptoKeyPair { + privateKey: CryptoKey; + publicKey: CryptoKey; +} +/* HMAC keys import from 'raw'; ECDSA and RSA keys generate, import and + * export as 'spki', 'pkcs8' and 'jwk', and ECDSA public keys as 'raw'. */ interface SubtleCrypto { digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise; + generateKey( + algorithm: EcKeyGenParams | RsaHashedKeyGenParams | "Ed25519" | { name: "Ed25519" }, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; importKey( format: "jwk", keyData: JsonWebKey, - algorithm: AlgorithmIdentifier | HmacImportParams, + algorithm: AlgorithmIdentifier | HmacImportParams | EcKeyImportParams | RsaHashedImportParams, extractable: boolean, keyUsages: readonly KeyUsage[], ): Promise; importKey( format: Exclude, keyData: BufferSource, - algorithm: AlgorithmIdentifier | HmacImportParams, + algorithm: AlgorithmIdentifier | HmacImportParams | EcKeyImportParams | RsaHashedImportParams, extractable: boolean, keyUsages: readonly KeyUsage[], ): Promise; - sign(algorithm: AlgorithmIdentifier, key: CryptoKey, data: BufferSource): Promise; + exportKey(format: "jwk", key: CryptoKey): Promise; + exportKey(format: Exclude, key: CryptoKey): Promise; + sign( + algorithm: AlgorithmIdentifier | EcdsaParams | RsaPssParams, + key: CryptoKey, + data: BufferSource, + ): Promise; verify( - algorithm: AlgorithmIdentifier, + algorithm: AlgorithmIdentifier | EcdsaParams | RsaPssParams, key: CryptoKey, signature: BufferSource, data: BufferSource, @@ -2693,8 +2737,20 @@ declare module "crypto" { import { Transform } from "stream"; export type KeyObjectType = "secret" | "public" | "private"; export interface JsonWebKey { - kty?: string; + crv?: string; + d?: string; + dp?: string; + dq?: string; + e?: string; k?: string; + kty?: string; + n?: string; + p?: string; + q?: string; + qi?: string; + x?: string; + y?: string; + [key: string]: unknown; } export interface JwkKeyExportOptions { format: "jwk"; @@ -2702,16 +2758,183 @@ declare module "crypto" { export interface KeyExportOptions { format?: "buffer" | undefined; } - /** Secret keys only; the asymmetric key types are not available. */ + export type KeyFormat = "pem" | "der" | "jwk"; + export interface AsymmetricKeyExportOptions { + type: "pkcs1" | "spki" | "pkcs8" | "sec1"; + format: T; + } + export interface AsymmetricKeyDetails { + modulusLength?: number; + publicExponent?: bigint; + namedCurve?: string; + } + /* Secret keys, and RSA and EC private and public keys. */ export class KeyObject { private constructor(); type: KeyObjectType; symmetricKeySize?: number; asymmetricKeyType?: string; + asymmetricKeyDetails?: AsymmetricKeyDetails; + export(options: AsymmetricKeyExportOptions<"pem">): string | Buffer; + export(options: AsymmetricKeyExportOptions<"der">): Buffer; export(options?: KeyExportOptions): Buffer; export(options: JwkKeyExportOptions): JsonWebKey; equals(otherKeyObject: KeyObject): boolean; } + export interface PrivateKeyInput { + key: string | Buffer; + format?: "pem" | "der" | undefined; + type?: "pkcs1" | "pkcs8" | "sec1" | undefined; + encoding?: string | undefined; + } + export interface PublicKeyInput { + key: string | Buffer; + format?: "pem" | "der" | undefined; + type?: "pkcs1" | "spki" | undefined; + encoding?: string | undefined; + } + export interface JsonWebKeyInput { + key: JsonWebKey; + format: "jwk"; + } + export function createPrivateKey( + key: PrivateKeyInput | string | Buffer | JsonWebKeyInput, + ): KeyObject; + export function createPublicKey( + key: PublicKeyInput | string | Buffer | KeyObject | JsonWebKeyInput, + ): KeyObject; + export interface KeyPairKeyObjectResult { + publicKey: KeyObject; + privateKey: KeyObject; + } + export interface KeyPairSyncResult { + publicKey: T1; + privateKey: T2; + } + export interface RSAKeyPairKeyObjectOptions { + modulusLength: number; + publicExponent?: number | undefined; + } + export interface ECKeyPairKeyObjectOptions { + namedCurve: string; + } + export interface RSAKeyPairOptions { + modulusLength: number; + publicExponent?: number | undefined; + publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF }; + privateKeyEncoding: { type: "pkcs1" | "pkcs8"; format: PrivF }; + } + export interface ECKeyPairOptions { + namedCurve: string; + publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF }; + privateKeyEncoding: { type: "sec1" | "pkcs8"; format: PrivF }; + } + export function generateKeyPairSync( + type: "rsa", + options: RSAKeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "rsa", + options: RSAKeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "rsa", + options: RSAKeyPairKeyObjectOptions, + ): KeyPairKeyObjectResult; + export function generateKeyPairSync( + type: "ec", + options: ECKeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "ec", + options: ECKeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "ec", + options: ECKeyPairKeyObjectOptions, + ): KeyPairKeyObjectResult; + export interface ED25519KeyPairOptions { + publicKeyEncoding: { type: "spki"; format: PubF }; + privateKeyEncoding: { type: "pkcs8"; format: PrivF }; + } + export function generateKeyPairSync( + type: "ed25519", + options: ED25519KeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "ed25519", + options: ED25519KeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + export function generateKeyPairSync(type: "ed25519", options?: {}): KeyPairKeyObjectResult; + export function generateKeyPair( + type: "ed25519", + options: {} | undefined, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + export function generateKeyPair( + type: "rsa", + options: RSAKeyPairKeyObjectOptions, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + export function generateKeyPair( + type: "ec", + options: ECKeyPairKeyObjectOptions, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; + export interface SigningOptions { + padding?: number | undefined; + saltLength?: number | undefined; + dsaEncoding?: "der" | "ieee-p1363" | undefined; + } + export interface SignPrivateKeyInput extends PrivateKeyInput, SigningOptions {} + export interface SignKeyObjectInput extends SigningOptions { + key: KeyObject; + } + export interface VerifyPublicKeyInput extends PublicKeyInput, SigningOptions {} + export interface VerifyKeyObjectInput extends SigningOptions { + key: KeyObject; + } + export function sign( + algorithm: string | null | undefined, + data: ArrayBufferView, + key: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput, + ): Buffer; + export function verify( + algorithm: string | null | undefined, + data: ArrayBufferView, + key: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput, + signature: ArrayBufferView, + ): boolean; + /* Sign and Verify accumulate update() data; they are not usable as + * writable streams. */ + export class Sign { + private constructor(); + update(data: string | ArrayBufferView): this; + update(data: string, inputEncoding: Encoding): this; + sign( + privateKey: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput, + ): Buffer; + sign( + privateKey: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput, + outputFormat: BinaryToTextEncoding, + ): string; + } + export class Verify { + private constructor(); + update(data: string | ArrayBufferView): this; + update(data: string, inputEncoding: Encoding): this; + verify( + object: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput, + signature: ArrayBufferView, + ): boolean; + verify( + object: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput, + signature: string, + signatureEncoding?: BinaryToTextEncoding, + ): boolean; + } + export function createSign(algorithm: string): Sign; + export function createVerify(algorithm: string): Verify; export function createSecretKey(key: ArrayBufferView | ArrayBufferLike): KeyObject; export function createSecretKey(key: string, encoding: BufferEncoding): KeyObject; export function generateKeySync(type: "hmac" | "aes", options: { length: number }): KeyObject; diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index f7cae8508d..f226073845 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -56,6 +56,8 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { tlsCa: features.tlsCa, cipher: features.cipher, ecdh: features.ecdh, + pki: features.pki, + ed25519: features.ed25519, webcrypto: features.webcrypto, perfLoop: features.perfLoop, }; diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index 805198df11..3867d255af 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -60,6 +60,8 @@ export async function compilePackedLibrary( tls: false, cipher: false, ecdh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, tlsCa: false, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8bb8bc3aab..c1a8a33b10 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -614,7 +614,9 @@ export const LIB_FN_SYMS: Record = { "crypto.generateKeySync": "scr_crypto_generate_key_sync", "crypto.keyObjectInput": "scr_crypto_key_object_input", "crypto.isKeyObject": "scr_crypto_is_key_object", + "webcrypto.generateKey": "scr_webcrypto_generate_key", "webcrypto.importKey": "scr_webcrypto_import_key", + "webcrypto.exportKey": "scr_webcrypto_export_key", "webcrypto.sign": "scr_webcrypto_sign", "webcrypto.verify": "scr_webcrypto_verify", "webcrypto.digest": "scr_webcrypto_digest", @@ -1027,6 +1029,14 @@ export const LIB_FN_SYMS: Record = { "crypto.x509ValidFromStr": "scr_crypto_x509_valid_from_str", "crypto.x509ValidTo": "scr_crypto_x509_valid_to", "crypto.x509ValidToStr": "scr_crypto_x509_valid_to_str", + "crypto.createPrivateKey": "scr_crypto_create_private_key", + "crypto.createPublicKey": "scr_crypto_create_public_key", + "crypto.generateKeyPairSync": "scr_crypto_generate_key_pair_sync", + "crypto.generateKeyPairCb": "scr_crypto_generate_key_pair_async", + "crypto.signOneshot": "scr_crypto_sign_oneshot", + "crypto.verifyOneshot": "scr_crypto_verify_oneshot", + "crypto.createSign": "scr_crypto_create_sign", + "crypto.createVerify": "scr_crypto_create_verify", "fs.writeFileModeSync": "scr_fs_write_file_mode", "fs.chownSync": "scr_fs_chown", "fs.lchownSync": "scr_fs_lchown", @@ -1565,6 +1575,7 @@ export function mockTimerSym( } export const USES_TIMERS_LIB_FNS = new Set([ + "crypto.generateKeyPairCb", "process.emitWarning", // Constructor-function streams defer their events on the stream ticks. "dyn.builtinStreamClass", @@ -1573,6 +1584,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ // ecdh.setPublicKey reports DEP0031 on the next tick. "crypto.ecdhNew", // WebCrypto results settle through the deferred crypto queue. + "webcrypto.generateKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index a476e1f7f2..484ce03cd7 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -90,6 +90,12 @@ export interface NativeLinkFeatures { /** crypto.createECDH and ECDH.convertKey: scr_ecdh.c plus the mbedTLS * archive. */ ecdh: boolean; + /** Asymmetric KeyObjects, crypto.sign/verify and Sign/Verify: scr_pki.c + * plus the mbedTLS archive. */ + pki: boolean; + /** Ed25519 keys and signatures with pki: scr_ed25519.c (the vendored + * libsodium Ed25519). */ + ed25519: boolean; /** perf_hooks event-loop metrics: scr_perf_loop.c. */ perfLoop: boolean; tlsCa: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 06a41c9b22..a4d5dc3dd9 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -249,6 +249,12 @@ export interface CcOptions { /** crypto.createECDH and ECDH.convertKey: compiles scr_ecdh.c and links * the mbedTLS archive (shared with tls). */ ecdh?: boolean; + /** Asymmetric KeyObjects and signatures: compiles scr_pki.c and links + * the mbedTLS archive. */ + pki?: boolean; + /** Ed25519 with pki: compiles scr_ed25519.c (the vendored libsodium + * Ed25519). */ + ed25519?: boolean; /** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */ webcrypto?: boolean; /** perf_hooks event-loop metrics: compiles scr_perf_loop.c. */ diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index a551ad600d..cae6d94996 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -114,6 +114,7 @@ interface RuntimeUnitContext { tlsCa: boolean; cipher: boolean; ecdh: boolean; + pki: boolean; lreObjects: readonly string[]; zlibObjects: readonly string[]; tlsArchive: string | null; @@ -297,6 +298,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"), unit((u) => u.opts.nodeTest, "scr_test.c"), unit((u) => u.opts.webcrypto, "scr_webcrypto.c"), + unit((u) => u.opts.webcrypto === true && u.opts.pki === true, "scr_webcrypto_pki.c"), + unit((u) => u.opts.pki === true && u.opts.ed25519 === true, "scr_ed25519.c"), // The CA-store unit rides its own gate OR the tls one: scr_tls.c // references its default-set override unconditionally. unit((u) => u.tlsCa, "scr_tls_ca.c"), @@ -308,6 +311,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ ...(u.tls ? [u.rt(join(u.rtDir, "scr_tls.c"))] : []), ...(u.cipher ? [u.rt(join(u.rtDir, "scr_cipher.c"))] : []), ...(u.ecdh ? [u.rt(join(u.rtDir, "scr_ecdh.c"))] : []), + ...(u.pki ? [u.rt(join(u.rtDir, "scr_pki.c"))] : []), u.tlsArchive, // mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h). // The unconditional win32 libs above do not carry it. @@ -375,7 +379,8 @@ export async function compileCInternal( // Symmetric ciphers and ECDH link the same mbedTLS archive as TLS. const cipher = opts.cipher ?? false; const ecdh = opts.ecdh ?? false; - const mbedtls = tls || cipher || ecdh; + const pki = opts.pki ?? false; + const mbedtls = tls || cipher || ecdh || pki; const driver = resolveCc(); const darwinDebugSymbols = needsDarwinDebugSymbols( targetPlatform(driver), @@ -733,6 +738,7 @@ export async function compileCInternal( tlsCa, cipher, ecdh, + pki, lreObjects, zlibObjects, tlsArchive, diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index fc7ed9efa0..0a6868aa02 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -125,6 +125,8 @@ const features = { tlsCa: false, cipher: false, ecdh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, }; diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 5ffb4797de..d0515a8efd 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -61,6 +61,8 @@ const BASE: NativeLinkFeatures = { tlsCa: false, cipher: false, ecdh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, }; diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts index b7049f6fe3..c9aecc170b 100644 --- a/packages/compiler/src/backend/target-diagnostics.ts +++ b/packages/compiler/src/backend/target-diagnostics.ts @@ -2,7 +2,11 @@ import { everyModuleNode, everyStmtList, transformStmtList } from "../ir/travers import type { LlvmUnsupportedError } from "./llvm/emitter.js"; import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; import { type IrModule, type SrcLoc } from "../ir/ir.js"; -import { moduleUsesFetch } from "../ir/runtime-features.js"; +import { + PKI_LIB_FNS, + moduleUsesFetch, + moduleUsesWebCryptoAsymmetric, +} from "../ir/runtime-features.js"; /** Reflection tables expose methods that the program never calls. On WASI, * keep their host-only implementations as explicit runtime refusals. Typed @@ -215,6 +219,14 @@ export function moduleWasiUnavailableSurface( }; return false; } + if (PKI_LIB_FNS.has(node.fn)) { + found = { + surface: + "asymmetric keys and signatures (the wasm32-wasi runtime does not include the mbedTLS library)", + loc, + }; + return false; + } if (node.fn === "os.networkInterfaces") { found = { surface: "network-interface enumeration (WASI Preview 1 has no interface API)", @@ -241,5 +253,12 @@ export function moduleWasiUnavailableSurface( if (moduleUsesFetch(mod)) { return { surface: "network-backed fetch (WASI Preview 1 has no socket API)", loc: entryLoc }; } + if (moduleUsesWebCryptoAsymmetric(mod)) { + return { + surface: + "WebCrypto ECDSA and RSA (the wasm32-wasi runtime does not include the mbedTLS library)", + loc: entryLoc, + }; + } return null; } diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index 2d8ae66417..2f98fd9200 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -67,6 +67,8 @@ export async function warmNativeCaches( tls: false, cipher: false, ecdh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, tlsCa: false, diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index 94b136c906..0d786e2087 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -65,6 +65,8 @@ const native: EarlyExecutableNativeFeatures = { tlsCa: false, cipher: false, ecdh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, }; diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 2312f5e85a..975f67cf81 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -96,6 +96,8 @@ export interface EarlyExecutableNativeFeatures { tls: boolean; cipher: boolean; ecdh: boolean; + pki: boolean; + ed25519: boolean; perfLoop: boolean; tlsCa: boolean; webcrypto: boolean; @@ -228,6 +230,8 @@ const BOOLEAN_NATIVE_KEYS = [ "tlsCa", "cipher", "ecdh", + "pki", + "ed25519", "webcrypto", "perfLoop", ] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[]; diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index f4270dbe82..8b42a218da 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -695,6 +695,8 @@ export function lowerCryptoModuleCall( loc, }; } + const pki = lowerPkiCall(lowerer, expr, bi.member, args, loc); + if (pki) return pki; if (bi.member === "createSecretKey") { if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) lowerer.noLowering( @@ -1335,3 +1337,55 @@ export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression): } return false; } + +/** 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, + * signatures and verdicts convert to their records, Buffers and + * booleans). Null for other members. */ +function lowerPkiCall( + lowerer: Lowerer, + expr: ts.CallExpression, + member: string, + args: readonly ts.Expression[], + loc: SrcLoc, +): IrExpr | null { + const forms: Readonly> = { + createPrivateKey: ["crypto.createPrivateKey", 1, 1, "createPrivateKey(key)"], + createPublicKey: ["crypto.createPublicKey", 1, 1, "createPublicKey(key)"], + generateKeyPairSync: ["crypto.generateKeyPairSync", 1, 2, "generateKeyPairSync(type, options)"], + generateKeyPair: [ + "crypto.generateKeyPairCb", + 2, + 3, + "generateKeyPair(type, [options,] callback)", + ], + sign: ["crypto.signOneshot", 3, 3, "sign(algorithm, data, key)"], + verify: ["crypto.verifyOneshot", 4, 4, "verify(algorithm, data, key, signature)"], + createSign: ["crypto.createSign", 1, 1, "createSign(algorithm)"], + createVerify: ["crypto.createVerify", 1, 1, "createVerify(algorithm)"], + }; + const form = forms[member]; + if (!form) return null; + const [fn, min, max, supported] = form; + if (args.length < min || args.length > max) + lowerer.noLowering( + `crypto.${member} with ${args.length} arguments`, + expr, + `${supported} is supported`, + ); + // generateKeyPair(type, callback): Node reads a function in the options + // position as the callback. + const shifted = fn === "crypto.generateKeyPairCb" && args.length === 2; + const values: IrExpr[] = []; + for (let i = 0; i < max; i++) { + const node = shifted ? (i === 1 ? undefined : i === 2 ? args[1] : args[i]) : args[i]; + values.push(dynArg(lowerer, node, loc)); + } + if (fn === "crypto.generateKeyPairCb") + 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); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/key-object.ts b/packages/compiler/src/frontend/lowering/builtins/key-object.ts index 7e680fa203..ecb4f275a3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/key-object.ts +++ b/packages/compiler/src/frontend/lowering/builtins/key-object.ts @@ -4,10 +4,11 @@ import { locOf } from "../../program.js"; import { BOOL, DYN, type IrExpr } from "../../../ir/ir.js"; import { builtinImportOf } from "./module-bindings.js"; -/* node:crypto's secret KeyObject is a native checked-dynamic handle - * (scr_lib.c): the KeyObject type maps to dyn, its getters and methods - * dispatch through the handle, and these helpers lower the class's uses - * that a dyn value cannot answer by itself. */ +/* node:crypto's KeyObject is a native checked-dynamic handle (secret keys + * in scr_lib.c, private and public keys in scr_pki.c), as are Sign and + * Verify: the types map to dyn, their getters and methods dispatch through + * the handle, and these helpers lower the uses that a dyn value cannot + * answer by itself. */ /** Whether `node` names node:crypto's KeyObject class. */ export function isKeyObjectClass(lowerer: Lowerer, node: ts.Expression): boolean { @@ -37,15 +38,31 @@ export function lowerKeyObjectInstanceof( } const KEY_OBJECT_METHODS = new Set(["export", "equals", "toCryptoKey"]); +const SIGNER_METHODS = new Set(["update", "sign", "verify"]); -/** `key.export(...)`, `key.equals(...)` and `key.toCryptoKey(...)`: the +/** Whether a value's checker type is node:crypto's Sign or Verify. */ +function isSignerValue(lowerer: Lowerer, node: ts.Expression): boolean { + const type = lowerer.typeOf(node); + const sym = type.getAliasSymbol() ?? type.getSymbol(); + return ( + !!sym && + (ts.symbolName(sym) === "Sign" || ts.symbolName(sym) === "Verify") && + lowerer.isStdlibSymbol(sym) + ); +} + +/** `key.export(...)`, `key.equals(...)` and `key.toCryptoKey(...)`, and + * the update/sign/verify methods of Sign and Verify (scr_pki.c): the * handle answers them as methods, so they dispatch through dynInvoke * rather than as a member read and a call. */ export function lowerKeyObjectCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null; const access = call.expression; - if (access.questionDotToken || !KEY_OBJECT_METHODS.has(access.name.text)) return null; - if (!isKeyObjectValue(lowerer, access.expression)) return null; + if (access.questionDotToken) return null; + const method = access.name.text; + const keyMethod = KEY_OBJECT_METHODS.has(method) && isKeyObjectValue(lowerer, access.expression); + if (!keyMethod && !(SIGNER_METHODS.has(method) && isSignerValue(lowerer, access.expression))) + return null; if (call.arguments.nodes.some(ts.isSpreadElement)) return null; return { kind: "dynInvoke", diff --git a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts index 93cba84c5f..ef04e8761c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts @@ -6,23 +6,27 @@ import { lowerDynObjectLiteral } from "../expressions/object-literals.js"; import { stdlibGlobalNameOf } from "../surfaces.js"; import { WEBCRYPTO_DYN_TYPES } from "../../type-mapper.js"; -/* WebCrypto's HMAC slice (scr_webcrypto.c): `crypto.subtle.importKey`, - * `sign`, `verify` and `digest` called on the crypto global (also through - * globalThis) lower to runtime entry points that take the argument vector - * as one dyn array and answer a promise, so Web IDL conversion, algorithm - * normalization and every error stay Node-shaped at run time. CryptoKey - * values are native handles; their type maps to dyn and their members - * are handle reads. Other SubtleCrypto methods are refused by name. */ +/* WebCrypto (scr_webcrypto.c, and scr_webcrypto_pki.c for ECDSA, Ed25519, + * RSASSA-PKCS1-v1_5 and RSA-PSS): `crypto.subtle.generateKey`, + * `importKey`, `exportKey`, `sign`, `verify` and `digest` called on the + * crypto global (also through globalThis) lower to runtime entry points + * that take the argument vector as one dyn array and answer a promise, so + * Web IDL conversion, algorithm normalization and every error stay + * Node-shaped at run time. CryptoKey values are native handles; their type + * maps to dyn and their members are handle reads. Other SubtleCrypto + * methods are refused by name. */ const SUBTLE_LOWERED: ReadonlyMap = new Map([ + ["generateKey", "webcrypto.generateKey"], ["importKey", "webcrypto.importKey"], + ["exportKey", "webcrypto.exportKey"], ["sign", "webcrypto.sign"], ["verify", "webcrypto.verify"], ["digest", "webcrypto.digest"], ]); const SUBTLE_HINT = - "the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; + "the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; /** Whether `node` is `crypto.subtle` on the WebCrypto global. */ function isSubtleReceiver(lowerer: Lowerer, node: ts.Expression): boolean { @@ -51,17 +55,6 @@ export function lowerWebCryptoCall(lowerer: Lowerer, call: ts.CallExpression): I if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`crypto.subtle.${member} with spread arguments`, call, SUBTLE_HINT); } - // JsonWebKey imports are not implemented: a literal 'jwk' format is - // refused here (the runtime refuses a computed one). - const format = call.arguments.nodes[0]; - if ( - member === "importKey" && - format !== undefined && - ts.isStringLiteralLike(format) && - format.text === "jwk" - ) { - lowerer.noLowering("crypto.subtle.importKey with the 'jwk' key format", format, SUBTLE_HINT); - } const loc = locOf(call); const args = call.arguments.nodes.map((arg) => ts.isObjectLiteralExpression(arg) diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index c779383376..bc5834533a 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1553,6 +1553,16 @@ export const BUILTIN_MODULE_FNS: Record< createSecretKey: { fn: "crypto.createSecretKey", params: [DYN, DYN], result: DYN }, generateKeySync: { fn: "crypto.generateKeySync", params: [DYN, DYN], result: DYN }, generateKey: { fn: "crypto.generateKeyCb", params: [], result: VOID }, + // Asymmetric keys and signatures (lowerPkiCall): checked-dynamic + // arguments, results converted to the call site's type. + createPrivateKey: { fn: "crypto.createPrivateKey", params: [DYN], result: DYN }, + createPublicKey: { fn: "crypto.createPublicKey", params: [DYN], result: DYN }, + createSign: { fn: "crypto.createSign", params: [DYN], result: DYN }, + createVerify: { fn: "crypto.createVerify", params: [DYN], result: DYN }, + generateKeyPairSync: { fn: "crypto.generateKeyPairSync", params: [], result: DYN }, + generateKeyPair: { fn: "crypto.generateKeyPairCb", params: [], result: VOID }, + sign: { fn: "crypto.signOneshot", params: [], result: DYN }, + verify: { fn: "crypto.verifyOneshot", params: [], result: DYN }, pbkdf2Sync: { fn: "crypto.pbkdf2", params: [BYTES_U8, BYTES_U8, F64, F64, STRING], @@ -2210,8 +2220,15 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ id: "stdlib.webcrypto.subtle", kind: "stdlib", name: "globalThis.crypto.subtle", - fns: ["webcrypto.importKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest"], - note: "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", + fns: [ + "webcrypto.generateKey", + "webcrypto.importKey", + "webcrypto.exportKey", + "webcrypto.sign", + "webcrypto.verify", + "webcrypto.digest", + ], + note: "importKey('raw') for HMAC keys, HMAC sign and verify, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", }, { id: "stdlib.webcrypto.CryptoKey", @@ -3055,14 +3072,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< crypto: { ...Object.fromEntries( [ - "generateKeyPair", - "generateKeyPairSync", - "createPrivateKey", - "createPublicKey", - "createSign", - "createVerify", - "sign", - "verify", "createDiffieHellman", "createDiffieHellmanGroup", "getDiffieHellman", @@ -3073,9 +3082,9 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< "privateDecrypt", ].map((m) => [ m, - "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and " + - "public and private KeyObjects — neither exists in the static runtime, so no faithful lowering " + - "can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics", + "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is " + + "RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, " + + "createSign, createVerify, and createECDH", ]), ), ...Object.fromEntries( diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index a233f0186d..50b809007a 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -146,6 +146,37 @@ export const WEBCRYPTO_DYN_TYPES: ReadonlySet = new Set([ "KeyAlgorithm", "HmacImportParams", "HmacKeyAlgorithm", + "CryptoKeyPair", + "EcKeyAlgorithm", + "RsaHashedKeyAlgorithm", + "EcKeyGenParams", + "EcKeyImportParams", + "EcdsaParams", + "RsaHashedKeyGenParams", + "RsaHashedImportParams", + "RsaPssParams", +]); + +/** node:crypto's key and signature option interfaces: the key functions, + * KeyObject.export and sign/verify take them as checked-dynamic arguments + * (scr_lib.c, scr_pki.c). */ +export const CRYPTO_KEY_OPTION_DYN_TYPES: ReadonlySet = new Set([ + "AsymmetricKeyExportOptions", + "ECKeyPairKeyObjectOptions", + "ECKeyPairOptions", + "ED25519KeyPairOptions", + "JsonWebKeyInput", + "JwkKeyExportOptions", + "KeyExportOptions", + "PrivateKeyInput", + "PublicKeyInput", + "RSAKeyPairKeyObjectOptions", + "RSAKeyPairOptions", + "SignKeyObjectInput", + "SignPrivateKeyInput", + "SigningOptions", + "VerifyKeyObjectInput", + "VerifyPublicKeyInput", ]); export const FETCH_AMBIENT_TYPES = [ @@ -2903,9 +2934,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ) { return DYN; } - // node:crypto's KeyObject: secret keys are native handles (scr_lib.c) - // whose getters and methods dispatch through the handle. The JsonWebKey - // that export() returns is a plain checked-dynamic object. + // node:crypto's KeyObject: secret keys (scr_lib.c) and private and + // public keys (scr_pki.c) are native handles whose getters and methods + // dispatch through the handle. The JsonWebKey that export() returns is a + // plain checked-dynamic object. if ( ((psym !== undefined && ts.symbolName(psym) === "KeyObject") || (psym !== undefined && ts.symbolName(psym) === "JsonWebKey")) && @@ -2917,6 +2949,35 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ) { return DYN; } + // node:crypto's key and signature options are plain checked-dynamic + // objects, so a value asserted to one (`opts as + // crypto.AsymmetricKeyExportOptions<"pem">`) reaches the call unchanged. + if ( + psym !== undefined && + CRYPTO_KEY_OPTION_DYN_TYPES.has(ts.symbolName(psym)) && + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + isDeclaredInAmbientModule(d, "crypto"), + ) + ) { + return DYN; + } + // node:crypto's Sign and Verify (createSign/createVerify): native + // handles over the digest state (scr_pki.c). + if ( + psym !== undefined && + (ts.symbolName(psym) === "Sign" || ts.symbolName(psym) === "Verify") && + psym.declarations.some( + (d) => + ts.isClassDeclaration(d) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + isDeclaredInAmbientModule(d, "crypto"), + ) + ) { + return DYN; + } if ( psym && (FETCH_AMBIENT_TYPES as readonly string[]).includes(ts.symbolName(psym)) && @@ -4183,6 +4244,41 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { }; } } + // crypto.generateKeyPairSync's results: KeyPairKeyObjectResult and + // KeyPairSyncResult as { publicKey, privateKey } records over the + // mapped property types (KeyObjects, PEM strings or DER Buffers), in + // Node's key order. + if ( + psym !== undefined && + (ts.symbolName(psym) === "KeyPairKeyObjectResult" || + ts.symbolName(psym) === "KeyPairSyncResult") && + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + isDeclaredInAmbientModule(d, "crypto"), + ) + ) { + const field = (name: string): IrType | null => { + const prop = ctx.checker.getPropertyOfType(type, name); + return prop ? mapType(ctx.checker.getTypeOfSymbol(prop), ctx) : null; + }; + const publicKey = field("publicKey"); + const privateKey = field("privateKey"); + if (!publicKey || !privateKey) return null; + return { + kind: "record", + shapeId: ctx.shapes.intern( + [ + { name: "privateKey", type: privateKey }, + { name: "publicKey", type: publicKey }, + ], + false, + undefined, + ["publicKey", "privateKey"], + ), + }; + } // crypto.X509Certificate — the Dirent-style data record: THREE visible // fields (fingerprint and the validFrom/validTo validity window, all // computed at construction by the new-expression lowering). Module- diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 2b216c77de..458930616f 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -624,6 +624,8 @@ async function compileExecutableNative( tlsCa: features.tlsCa, cipher: features.cipher, ecdh: features.ecdh, + pki: features.pki, + ed25519: features.ed25519, webcrypto: features.webcrypto, perfLoop: features.perfLoop, ...(onArtifactReady === undefined ? {} : { onArtifactReady }), diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index ca8c521186..3d9fb3f50b 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -976,4 +976,12 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "crypto.x509ValidFromStr", "crypto.x509ValidTo", "crypto.x509ValidToStr", + "crypto.createPrivateKey", + "crypto.createPublicKey", + "crypto.generateKeyPairSync", + "crypto.generateKeyPairCb", + "crypto.signOneshot", + "crypto.verifyOneshot", + "crypto.createSign", + "crypto.createVerify", ]); diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fa78765791..95de752ec2 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2845,7 +2845,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** WebCrypto (scr_webcrypto.c): each method takes its argument vector as * one dyn array and answers a promise that argument and validation * failures reject. CryptoKey values are native handles. */ + "webcrypto.generateKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.importKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.exportKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.sign": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.verify": { argTypes: [DYN], result: { kind: "promise", inner: BOOL } }, "webcrypto.digest": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, @@ -4638,6 +4640,19 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * body passes its undefined result on into a Buffer slot. Owned (+1); * never throws. */ "bytes.absent": { argTypes: [], result: BYTES_U8 }, + /** node:crypto asymmetric keys and signatures (scr_pki.c): checked- + * dynamic arguments, borrowed; KeyObject, Sign and Verify handles, + * Buffers and booleans as checked-dynamic results (+1). Node's argument + * and decoder errors throw (may-throw); generateKeyPairCb's callback + * receives (null, publicKey, privateKey) in a later macrotask. */ + "crypto.createPrivateKey": { argTypes: [DYN], result: DYN }, + "crypto.createPublicKey": { argTypes: [DYN], result: DYN }, + "crypto.generateKeyPairSync": { argTypes: [DYN, DYN], result: DYN }, + "crypto.generateKeyPairCb": { argTypes: [DYN, DYN, DYN], result: VOID }, + "crypto.signOneshot": { argTypes: [DYN, DYN, DYN], result: DYN }, + "crypto.verifyOneshot": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, + "crypto.createSign": { argTypes: [DYN], result: DYN }, + "crypto.createVerify": { argTypes: [DYN], result: DYN }, /** The Buffer brand of a Uint8Array source onto a fresh Uint8Array * result (map and filter of a Buffer return a Buffer). */ "bytes.copyBrand": { argTypes: [BYTES_U8, BYTES_U8], result: VOID }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 4142b6efe3..2e828c0279 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -621,6 +621,63 @@ 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_pki.c entry points (the pki unit). */ +export const PKI_LIB_FNS: ReadonlySet = new Set([ + "crypto.createPrivateKey", + "crypto.createPublicKey", + "crypto.createSign", + "crypto.createVerify", + "crypto.generateKeyPairCb", + "crypto.generateKeyPairSync", + "crypto.signOneshot", + "crypto.verifyOneshot", +]); + +/** The WebCrypto algorithms scr_webcrypto_pki.c implements over the pki + * unit (lowercased). */ +const WEBCRYPTO_ASYMMETRIC_NAMES: ReadonlySet = new Set([ + "ecdsa", + "ed25519", + "rsa-pss", + "rsassa-pkcs1-v1_5", +]); + +/** Whether a string literal in the module names Ed25519, without regard + * to case ("ed25519" for node:crypto, "Ed25519" for WebCrypto): the + * Ed25519 unit links then, alongside the pki unit. */ +export function moduleNamesEd25519(mod: IrModule): boolean { + let named = false; + everyModuleNode(mod, { + expr: (node) => { + if (node.kind === "strLit" && node.value.toLowerCase() === "ed25519") named = true; + return !named; + }, + stmt: () => !named, + type: () => true, + }); + return named; +} + +/** Whether the module calls WebCrypto and a string literal in it names one + * of the asymmetric algorithms (matched without regard to case, as + * normalizeAlgorithm matches names): scr_webcrypto_pki.c and the pki unit + * link then. */ +export function moduleUsesWebCryptoAsymmetric(mod: IrModule): boolean { + let webcrypto = false; + let named = false; + everyModuleNode(mod, { + expr: (node) => { + if (node.kind === "libCall" && node.fn.startsWith("webcrypto.")) webcrypto = true; + if (node.kind === "strLit" && WEBCRYPTO_ASYMMETRIC_NAMES.has(node.value.toLowerCase())) + named = true; + return !(webcrypto && named); + }, + stmt: () => !(webcrypto && named), + type: () => true, + }); + return webcrypto && named; +} + /** One rule per runtime feature, sorted by name. A new feature is an * entry here and its line in noRuntimeFeatures; RuntimeFeatures, the scan * and its early exit derive from this table. The lists above keep one @@ -819,6 +876,12 @@ const RUNTIME_FEATURE_RULES = { /** perf_hooks event-loop metrics (eventLoopUtilization, * monitorEventLoopDelay): scr_perf_loop.c. */ perfLoop: rule({ fns: ["perf.idleTime"], prefixes: ["perf.loop", "perf.eld"] }), + /** scr_pki.c over the mbedTLS archive: asymmetric KeyObjects and + * signatures. */ + pki: rule({ initial: moduleUsesWebCryptoAsymmetric, fns: [...PKI_LIB_FNS] }), + /** scr_ed25519.c (the vendored libsodium Ed25519), linked with pki: a + * string literal in the module names Ed25519. */ + ed25519: rule({ initial: moduleNamesEd25519 }), processEvents: rule({ fns: [...PROCESS_EVENT_LIB_FNS, "util.styleText"], types: ["procStream"] }), /** scr_url_legacy.c parses and stringifies queries. */ qs: rule({ fns: [...QS_LIB_FNS, ...LEGACY_URL_LIB_FNS] }), @@ -926,6 +989,8 @@ function noRuntimeFeatures(): RuntimeFeatures { nodeTest: false, parseArgs: false, perfLoop: false, + pki: false, + ed25519: false, processEvents: false, qs: false, regex: false, diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index 51aeb6146e..f16ae3d0cf 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -52,6 +52,13 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["tls.", "the node:tls surface"], ["crypto.cipherNew", "crypto.createCipheriv and createDecipheriv"], ["crypto.ecdh", "crypto.createECDH and ECDH.convertKey"], + ["crypto.createPrivateKey", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.createPublicKey", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.createSign", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.createVerify", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.generateKeyPair", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.signOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.verifyOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["webcrypto.", "WebCrypto (crypto.subtle)"], ["fs.watch", "fs.watch"], ["watcher.", "fs.watch"], @@ -159,6 +166,7 @@ export function moduleLibAsyncSurface(mod: IrModule): { surface: string; loc: Sr [features.fsWatch, "fs.watch"], [features.stream, "the node:stream surface"], [features.tls, "the node:tls surface"], + [features.pki, "asymmetric KeyObjects, generateKeyPair, sign and verify"], [features.fetch, "fetch"], [features.eventTarget, "the EventTarget surface"], [features.nodeTest, "the node:test surface"], diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index ed8d52293f..7679338bbd 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -110,6 +110,8 @@ async function main(): Promise { tls: false, cipher: false, ecdh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, tlsCa: false, diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index 6b0a2fea54..f470b614e9 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -161,6 +161,8 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "tlsCa", "cipher", "ecdh", + "pki", + "ed25519", "webcrypto", "perfLoop", ].every((name) => typeof features[name] === "boolean"); diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index e6beb24ccc..a626cd78c5 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -120,6 +120,8 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( tlsCa: false, cipher: false, ecdh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, nativeFetch: false, diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index c05e994a8f..a1f252814e 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -498,7 +498,7 @@ "name": "crypto.createDiffieHellman", "status": "unsupported", "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH" }, { "id": "node-builtin.crypto.createDiffieHellmanGroup", @@ -506,7 +506,7 @@ "name": "crypto.createDiffieHellmanGroup", "status": "unsupported", "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH" }, { "id": "node-builtin.crypto.createECDH", @@ -530,17 +530,13 @@ "id": "node-builtin.crypto.createPrivateKey", "kind": "node-builtin", "name": "crypto.createPrivateKey", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.createPublicKey", "kind": "node-builtin", "name": "crypto.createPublicKey", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.createSecretKey", @@ -552,17 +548,13 @@ "id": "node-builtin.crypto.createSign", "kind": "node-builtin", "name": "crypto.createSign", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.createVerify", "kind": "node-builtin", "name": "crypto.createVerify", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.diffieHellman", @@ -570,7 +562,7 @@ "name": "crypto.diffieHellman", "status": "unsupported", "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH" }, { "id": "node-builtin.crypto.generateKey", @@ -582,17 +574,13 @@ "id": "node-builtin.crypto.generateKeyPair", "kind": "node-builtin", "name": "crypto.generateKeyPair", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.generateKeyPairSync", "kind": "node-builtin", "name": "crypto.generateKeyPairSync", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.generateKeySync", @@ -614,7 +602,7 @@ "name": "crypto.getDiffieHellman", "status": "unsupported", "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH" }, { "id": "node-builtin.crypto.getRandomValues", @@ -658,7 +646,7 @@ "name": "crypto.privateDecrypt", "status": "unsupported", "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH" }, { "id": "node-builtin.crypto.privateEncrypt", @@ -666,7 +654,7 @@ "name": "crypto.privateEncrypt", "status": "unsupported", "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH" }, { "id": "node-builtin.crypto.publicDecrypt", @@ -674,7 +662,7 @@ "name": "crypto.publicDecrypt", "status": "unsupported", "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH" }, { "id": "node-builtin.crypto.publicEncrypt", @@ -682,7 +670,7 @@ "name": "crypto.publicEncrypt", "status": "unsupported", "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH" }, { "id": "node-builtin.crypto.randomBytes", @@ -744,9 +732,7 @@ "id": "node-builtin.crypto.sign", "kind": "node-builtin", "name": "crypto.sign", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.timingSafeEqual", @@ -758,9 +744,7 @@ "id": "node-builtin.crypto.verify", "kind": "node-builtin", "name": "crypto.verify", - "status": "unsupported", - "code": "SC2020", - "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" + "status": "static" }, { "id": "node-builtin.crypto.webcrypto", @@ -5726,7 +5710,7 @@ "kind": "stdlib", "name": "globalThis.crypto.subtle", "status": "static", - "note": "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" + "note": "importKey('raw') for HMAC keys, HMAC sign and verify, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" }, { "id": "syntax.async-generators", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7080-crypto-ed25519.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7080-crypto-ed25519.mjs.json new file mode 100644 index 0000000000..035d9d7651 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7080-crypto-ed25519.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7080-crypto-ed25519.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7081-webcrypto-ed25519.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7081-webcrypto-ed25519.mjs.json new file mode 100644 index 0000000000..bbd2d1ebc0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7081-webcrypto-ed25519.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7081-webcrypto-ed25519.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7082-crypto-key-objects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7082-crypto-key-objects.ts.json new file mode 100644 index 0000000000..c2e50cc976 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7082-crypto-key-objects.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7082-crypto-key-objects.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7083-crypto-sign-verify.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7083-crypto-sign-verify.ts.json new file mode 100644 index 0000000000..385c9347c8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7083-crypto-sign-verify.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7083-crypto-sign-verify.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7084-crypto-key-pairs.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7084-crypto-key-pairs.cjs.json new file mode 100644 index 0000000000..e1ac905369 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7084-crypto-key-pairs.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7084-crypto-key-pairs.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7085-webcrypto-ecdsa-rsa.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7085-webcrypto-ecdsa-rsa.ts.json new file mode 100644 index 0000000000..3fc5406e88 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7085-webcrypto-ecdsa-rsa.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7085-webcrypto-ecdsa-rsa.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs.json new file mode 100644 index 0000000000..ef44f91809 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/keys.ts.json new file mode 100644 index 0000000000..b0b79568d4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-ecdsa.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-ecdsa.ts.json new file mode 100644 index 0000000000..2c4a3b15bc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-ecdsa.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/webcrypto-ecdsa.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index db72284f69..5549b0db95 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -43,7 +43,7 @@ export const RUNTIME_PACK_MATRIX = withLibraryRuntimeFlavors( { id: "quickjs", predicate: false }, { id: "libregexp", predicate: "regex" }, { id: "zlib", predicate: "zlibEffective" }, - { id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh") }, + { id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh", "pki") }, ], system_libraries: [{ name: "System", predicate: true }], }), diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 85c5ffb000..9e74b48c6b 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -172,7 +172,8 @@ async function build() { ...(unit.source === "scr_regex.c" ? ["-I", quickjs] : []), ...(unit.source === "scr_tls.c" || unit.source === "scr_cipher.c" || - unit.source === "scr_ecdh.c" + unit.source === "scr_ecdh.c" || + unit.source === "scr_pki.c" ? ["-I", join(mbedtls, "include")] : []), ...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c" diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 0f40218a18..0039662bfc 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -149,8 +149,11 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_tls.c", "tlsEffective"], ["scr_cipher.c", "cipher"], ["scr_ecdh.c", "ecdh"], + ["scr_pki.c", "pki"], ["scr_wasm.c", "wasm"], ["scr_webcrypto.c", "webcrypto"], + ["scr_webcrypto_pki.c", all("webcrypto", "pki")], + ["scr_ed25519.c", all("pki", "ed25519")], ["scr_fetch.c", "nativeFetch"], ]; @@ -212,7 +215,7 @@ export function createRuntimePackMatrix({ { id: "quickjs", predicate: false }, { id: "libregexp", predicate: "regex" }, { id: "zlib", predicate: "zlibEffective" }, - { id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh") }, + { id: "mbedtls", predicate: any("tlsEffective", "cipher", "ecdh", "pki") }, ]; const matrix = withLibraryRuntimeFlavors( withWorkerRuntimeVariants({ diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index b242b78aa5..3b43f3b83e 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -223,7 +223,8 @@ async function build() { ...(unit.source === "scr_regex.c" ? ["-I", quickjs] : []), ...(unit.source === "scr_tls.c" || unit.source === "scr_cipher.c" || - unit.source === "scr_ecdh.c" + unit.source === "scr_ecdh.c" || + unit.source === "scr_pki.c" ? ["-I", join(mbedtls, "include")] : []), ...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c" diff --git a/packages/runtime-wasm32-wasi/scripts/build.mjs b/packages/runtime-wasm32-wasi/scripts/build.mjs index 2dd9839731..7c8bc46525 100644 --- a/packages/runtime-wasm32-wasi/scripts/build.mjs +++ b/packages/runtime-wasm32-wasi/scripts/build.mjs @@ -22,6 +22,9 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ "scr_tls.c", "scr_cipher.c", "scr_ecdh.c", + "scr_pki.c", + "scr_webcrypto_pki.c", + "scr_ed25519.c", "scr_wasm.c", "scr_fetch.c", ], diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index d12858abfa..29695e5cce 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -82,6 +82,10 @@ ScrDyn *scr_crypto_create_secret_key(ScrDyn *key, ScrDyn *encoding); ScrDyn *scr_crypto_generate_key_sync(ScrDyn *type, ScrDyn *options); ScrBytes *scr_crypto_key_object_input(ScrDyn *key); bool scr_crypto_is_key_object(const ScrDyn *value); +/* Throws Node's ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE for a private or public + * KeyObject used as a secret key. scr_pki.c, which makes those keys, + * installs it with them, so its messages stay out of other programs. */ +extern void (*scr_crypto_reject_asymmetric_secret)(const ScrDyn *key); const unsigned char *scr_key_object_bytes(const ScrDyn *value, size_t *len); /* crypto.createCipheriv (decrypt false) and crypto.createDecipheriv * (decrypt true) over checked-dynamic arguments (scr_cipher.c, linked @@ -212,6 +216,8 @@ size_t scr_crypto_hmac_raw(const char *alg, const unsigned char *key, size_t key * verify with a boolean on a later loop turn. scr_webcrypto_is_crypto_key * answers `value instanceof CryptoKey`. */ ScrPromise *scr_webcrypto_import_key(ScrDyn *args); +ScrPromise *scr_webcrypto_generate_key(ScrDyn *args); +ScrPromise *scr_webcrypto_export_key(ScrDyn *args); ScrPromise *scr_webcrypto_sign(ScrDyn *args); ScrPromise *scr_webcrypto_verify(ScrDyn *args); ScrPromise *scr_webcrypto_digest(ScrDyn *args); @@ -229,5 +235,17 @@ ScrStr *scr_crypto_x509_valid_from(ScrBytes *data); ScrStr *scr_crypto_x509_valid_from_str(ScrStr *pem); ScrStr *scr_crypto_x509_valid_to(ScrBytes *data); ScrStr *scr_crypto_x509_valid_to_str(ScrStr *pem); +/* Asymmetric keys and signatures (scr_pki.c, the pki unit). Arguments + * borrowed; results +1, or NULL with the error pending. */ +ScrDyn *scr_crypto_create_private_key(ScrDyn *key); +ScrDyn *scr_crypto_create_public_key(ScrDyn *key); +ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options); +void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *callback); +ScrDyn *scr_crypto_sign_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key); +ScrDyn *scr_crypto_verify_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key, ScrDyn *signature); +ScrDyn *scr_crypto_create_sign(ScrDyn *algorithm); +ScrDyn *scr_crypto_create_verify(ScrDyn *algorithm); +bool scr_crypto_is_asymmetric_key(const ScrDyn *value); +const char *scr_crypto_asymmetric_key_type(const ScrDyn *value); #endif /* SCR_CRYPTO_H */ diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 02c4d828f2..b2615b57ee 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -158,6 +158,11 @@ typedef enum { X(DIR, 44) /* fs.Dir (scr_lib.c) */ \ X(DISPOSABLE_STACK, 49) /* DisposableStack (scr_symbol.c) */ \ X(ECDH, 64) /* node:crypto ECDH (scr_ecdh.c) */ \ + X(PRIVATE_KEY, 65) /* node:crypto PrivateKeyObject (scr_pki.c) */ \ + X(PUBLIC_KEY, 66) /* node:crypto PublicKeyObject (scr_pki.c) */ \ + X(SIGN, 67) /* node:crypto Sign (scr_pki.c) */ \ + X(VERIFY, 68) /* node:crypto Verify (scr_pki.c) */ \ + X(ASYMMETRIC_CRYPTO_KEY, 69) /* WebCrypto ECDSA and RSA CryptoKey (scr_webcrypto_pki.c) */ \ X(EVENT, 14) /* ScrEvent — new Event() and AbortSignal's abort events */ \ X(EVENT_TARGET, 39) /* ScrEventTarget — new EventTarget() */ \ X(FETCH_HEADERS, 13) /* native static-fetch response Headers */ \ diff --git a/packages/runtime/src/scr_ed25519.c b/packages/runtime/src/scr_ed25519.c new file mode 100644 index 0000000000..c6d6ce370f --- /dev/null +++ b/packages/runtime/src/scr_ed25519.c @@ -0,0 +1,76 @@ +/* Ed25519 for scr_pki.c: libsodium's ref10 implementation (vendored, see + * ../vendor/README.md), textually included so it builds as this one unit. + * It links only into programs that name Ed25519 (the ed25519 runtime + * feature) and installs itself into scr_pki.c at startup; without it, + * Ed25519 signing, verification and public-key derivation throw a coded + * SC2020 error. + * + * Signing runs in constant time. Verification rejects a non-canonical S + * and the small-order R and public-key encodings, then checks the + * cofactorless equation, which is what Node 24 answers. */ +#include "scr_runtime.h" + +#include +#include +#include +#include + +#include "scr_pki.h" + +#define CONFIGURED 1 +#include "../vendor/libsodium/ed25519_ref10.c" +#include "../vendor/libsodium/hash_sha512_cp.c" +#include "../vendor/libsodium/verify.c" +#include "../vendor/libsodium/sign.c" +#include "../vendor/libsodium/open.c" +#include "../vendor/libsodium/keypair.c" +#undef CONFIGURED + +/* The libsodium utilities those units call. */ +void sodium_memzero(void *const pnt, const size_t len) { + volatile unsigned char *p = (volatile unsigned char *)pnt; + for (size_t i = 0; i < len; i++) p[i] = 0U; +} + +int sodium_memcmp(const void *const b1_, const void *const b2_, size_t len) { + const volatile unsigned char *b1 = (const volatile unsigned char *)b1_; + const volatile unsigned char *b2 = (const volatile unsigned char *)b2_; + volatile unsigned char d = 0U; + for (size_t i = 0; i < len; i++) d |= b1[i] ^ b2[i]; + return (1 & ((d - 1) >> 8)) - 1; +} + +int sodium_is_zero(const unsigned char *n, const size_t nlen) { + volatile unsigned char d = 0U; + for (size_t i = 0; i < nlen; i++) d |= n[i]; + return 1 & ((d - 1) >> 8); +} + +void randombytes_buf(void *const buf, const size_t size) { arc4random_buf(buf, size); } + +static void se_public_key(uint8_t pub[32], const uint8_t seed[32]) { + uint8_t sk[64]; + crypto_sign_ed25519_seed_keypair(pub, sk, seed); + sodium_memzero(sk, sizeof sk); +} + +static void se_sign(uint8_t sig[64], const uint8_t seed[32], const uint8_t pub[32], const uint8_t *m, + size_t n) { + uint8_t sk[64]; + memcpy(sk, seed, 32); + memcpy(sk + 32, pub, 32); + crypto_sign_ed25519_detached(sig, NULL, m, n, sk); + sodium_memzero(sk, sizeof sk); +} + +static bool se_verify(const uint8_t pub[32], const uint8_t *m, size_t n, const uint8_t sig[64]) { + return crypto_sign_ed25519_verify_detached(sig, m, n, pub) == 0; +} + +static const ScrPkiEd25519 se_table = { + .public_key = se_public_key, + .sign = se_sign, + .verify = se_verify, +}; + +__attribute__((constructor)) static void se_install(void) { scr_pki_ed25519 = &se_table; } diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index ee4c345925..094ec36af4 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -9225,6 +9225,10 @@ ScrCryptoHash *scr_crypto_hmac_new_dyn(ScrStr *alg, ScrDyn *key, ScrDyn *options if (key_object) { bytes = scr_bytes_new(SCR_BYTES_U8, (double)key_object_len); if (key_object_len) memcpy(bytes->data, key_object, key_object_len); + } else if (scr_crypto_is_key_object(key)) { + /* A private or public KeyObject: scr_pki.c reports Node's error. */ + scr_crypto_reject_asymmetric_secret(key); + return NULL; } else if (key->kind == SCR_DYN_STR) { ScrStr *utf8 = encoding ? NULL : scr_str_new("utf8", 4); bytes = scr_bytes_from_str_checked(key->v.str, encoding ? encoding : utf8); @@ -9856,9 +9860,15 @@ const unsigned char *scr_key_object_bytes(const ScrDyn *value, size_t *len) { return key->data; } -/* `value instanceof KeyObject`. */ +void (*scr_crypto_reject_asymmetric_secret)(const ScrDyn *key) = NULL; + +/* `value instanceof KeyObject`: a secret key, or a private or public key + * (scr_pki.c, matched by its handle tags so this unit does not link it). */ bool scr_crypto_is_key_object(const ScrDyn *value) { size_t len; + if (value->kind == SCR_DYN_HANDLE && + (value->v.handle.tag == SCR_DYNH_PRIVATE_KEY || value->v.handle.tag == SCR_DYNH_PUBLIC_KEY)) + return true; return scr_key_object_bytes(value, &len) != NULL; } diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c new file mode 100644 index 0000000000..1b102fd22c --- /dev/null +++ b/packages/runtime/src/scr_pki.c @@ -0,0 +1,2118 @@ +/* node:crypto asymmetric keys and signatures over the vendored mbedTLS + * public-key layer: createPrivateKey, createPublicKey, generateKeyPairSync + * and generateKeyPair for "rsa", "ec" and "ed25519" keys, the + * PrivateKeyObject and PublicKeyObject handles (type, asymmetricKeyType, + * asymmetricKeyDetails, export(), equals()), crypto.sign and crypto.verify, + * and the Sign and Verify objects of createSign and createVerify. Keys read + * and write PEM and DER as PKCS#1, SEC1, PKCS#8 and SubjectPublicKeyInfo, + * and JWK; the EC curves are prime256v1, secp384r1, secp521r1 and + * secp256k1, and the Ed25519 arithmetic is scr_ed25519.c. Argument checks + * and errors follow Node 24's lib/internal/crypto/keys.js, keygen.js and + * sig.js. Encrypted keys and other key types throw a coded SC2020 error. + * All arguments are borrowed. */ +#include "scr_runtime.h" + +#include +#include +#include + +#include "mbedtls/asn1.h" +#include "mbedtls/asn1write.h" +#include "mbedtls/base64.h" +#include "mbedtls/bignum.h" +#include "mbedtls/ecdsa.h" +#include "mbedtls/ecp.h" +#include "mbedtls/md.h" +#include "mbedtls/oid.h" +#include "mbedtls/pk.h" +#include "mbedtls/platform_util.h" +#include "mbedtls/rsa.h" +#include "mbedtls/x509_crt.h" + +#include "scr_pki.h" + +/* ── errors ───────────────────────────────────────────────────────────── */ + +static void sp_throw(int kind, const char *code, const char *message) { + scr_throw_error_msg_code(kind, message, strlen(message), code); +} + +/* ERR_INVALID_ARG_VALUE for an option ("The property 'options.type' is + * invalid. Received 'spki'"). */ +static void sp_property_invalid(const char *name, const ScrDyn *got) { + char lite[96]; + char message[256]; + int n = snprintf(message, sizeof message, "The property '%s' is invalid. Received %s", name, + scr_dyn_inspect_lite(got, lite, sizeof lite)); + scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)n, "ERR_INVALID_ARG_VALUE"); +} + +static void sp_unsupported_input(void) { + sp_throw(SCR_ERR_ERROR, "ERR_OSSL_UNSUPPORTED", "error:1E08010C:DECODER routines::unsupported"); +} + +/* The coded refusal for a form this runtime does not implement. */ +static void sp_not_supported(const char *what) { + char message[256]; + int n = snprintf(message, sizeof message, + "'%s' is part of the standard library types but has no scriptc lowering yet", what); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020"); +} + +static int sp_random(void *state, unsigned char *out, size_t len) { + (void)state; + arc4random_buf(out, len); + return 0; +} + +/* ── Ed25519 ────────────────────────────────────────────────────────── + * The arithmetic is scr_ed25519.c, which links only into programs that + * name Ed25519 and installs itself here at startup. Keys are the 32-byte + * seed and the 32-byte encoded public point. */ +const ScrPkiEd25519 *scr_pki_ed25519 = NULL; + +/* The Ed25519 arithmetic, or NULL with a coded SC2020 error pending. */ +static const ScrPkiEd25519 *sp_ed(void) { + if (!scr_pki_ed25519) + sp_throw(SCR_ERR_ERROR, "SC2020", + "Ed25519 keys are supported when a string literal in the program names 'ed25519'"); + return scr_pki_ed25519; +} + +bool scr_pki_ed25519_ready(void) { return sp_ed() != NULL; } + +static bool sp_ed_verify(const ScrPkiEd25519 *ed, const uint8_t pub[32], const uint8_t *m, size_t n, + const uint8_t *sig, size_t sig_len) { + return sig_len == 64 && ed->verify(pub, m, n, sig); +} + +/* ── option bags ──────────────────────────────────────────────────────── */ + +static bool sp_is_object(const ScrDyn *v) { return v != NULL && v->kind == SCR_DYN_OBJ; } + +/* A statically typed object argument (a record or class instance) as a + * plain object: +1, or NULL with the error pending. Other values are + * retained as they are. */ +static ScrDyn *sp_plain(const ScrDyn *v) { + if (v != NULL && v->kind == SCR_DYN_TYPED_REF) return scr_dyn_typed_ref_materialize(v); + return scr_dyn_retain((ScrDyn *)(v ? v : scr_dyn_undefined())); +} + +/* A borrowed option, or the undefined singleton. */ +static const ScrDyn *sp_opt(const ScrDyn *obj, const char *key) { + const ScrDyn *v = sp_is_object(obj) ? scr_dyn_obj_get(obj, key, strlen(key)) : NULL; + return v ? v : scr_dyn_undefined(); +} + +static bool sp_is_str(const ScrDyn *v, const char *text) { + size_t n = strlen(text); + return v->kind == SCR_DYN_STR && v->v.str->len == n && memcmp(v->v.str->data, text, n) == 0; +} + +static bool sp_absent(const ScrDyn *v) { return v->kind == SCR_DYN_UNDEF; } + +/* ── keys ─────────────────────────────────────────────────────────────── */ + +typedef struct SpKey { + size_t rc; + mbedtls_pk_context pk; /* RSA and EC keys */ + bool private_key; + bool ed25519; /* an Ed25519 key: ed_seed (private keys) and ed_pub */ + uint8_t ed_seed[32]; + uint8_t ed_pub[32]; +} SpKey; + +static void *sp_key_retain(void *h) { + SpKey *k = h; + if (k->rc != SIZE_MAX) k->rc++; + return h; +} + +static void sp_key_release(void *h) { + SpKey *k = h; + if (!k || k->rc == SIZE_MAX || --k->rc != 0) return; + mbedtls_pk_free(&k->pk); + mbedtls_platform_zeroize(k, sizeof *k); + free(k); +} + +static SpKey *sp_key_alloc(bool private_key) { + SpKey *k = calloc(1, sizeof *k); + if (!k) scr_trap("scriptc: out of memory\n"); + k->rc = 1; + k->private_key = private_key; + mbedtls_pk_init(&k->pk); + return k; +} + +static void sp_key_install(void); + +/* Boxes the key (consuming the caller's reference). */ +static ScrDyn *sp_key_box(SpKey *k) { + sp_key_install(); + ScrDyn *box = scr_dyn_new_handle(k, k->private_key ? SCR_DYNH_PRIVATE_KEY : SCR_DYNH_PUBLIC_KEY); + sp_key_release(k); + return box; +} + +static SpKey *sp_key_of(const ScrDyn *v) { + if (v == NULL || v->kind != SCR_DYN_HANDLE) return NULL; + if (v->v.handle.tag != SCR_DYNH_PRIVATE_KEY && v->v.handle.tag != SCR_DYNH_PUBLIC_KEY) return NULL; + return v->v.handle.ptr; +} + +bool scr_crypto_is_asymmetric_key(const ScrDyn *value) { return sp_key_of(value) != NULL; } + +/* The type getter of a KeyObject value: "private", "public", or NULL for + * anything else. */ +const char *scr_crypto_asymmetric_key_type(const ScrDyn *value) { + SpKey *k = sp_key_of(value); + return k ? (k->private_key ? "private" : "public") : NULL; +} + +static bool sp_is_rsa(const SpKey *k) { return mbedtls_pk_get_type(&k->pk) == MBEDTLS_PK_RSA; } + +static bool sp_is_ec(const SpKey *k) { return mbedtls_pk_get_type(&k->pk) == MBEDTLS_PK_ECKEY; } + +static bool sp_is_ed(const SpKey *k) { return k->ed25519; } + +typedef struct { + const char *name; /* OpenSSL short name (namedCurve) */ + const char *nist; /* EC_curve_nist2nid alias, or NULL */ + const char *jwk; /* JWK crv */ + mbedtls_ecp_group_id id; +} SpCurve; + +static const SpCurve sp_curves[] = { + {"prime256v1", "P-256", "P-256", MBEDTLS_ECP_DP_SECP256R1}, + {"secp384r1", "P-384", "P-384", MBEDTLS_ECP_DP_SECP384R1}, + {"secp521r1", "P-521", "P-521", MBEDTLS_ECP_DP_SECP521R1}, + {"secp256k1", NULL, "secp256k1", MBEDTLS_ECP_DP_SECP256K1}, +}; + +static const SpCurve *sp_curve_by_id(mbedtls_ecp_group_id id) { + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (sp_curves[i].id == id) return &sp_curves[i]; + return NULL; +} + +static const SpCurve *sp_curve_of(const SpKey *k) { + return sp_is_ec(k) ? sp_curve_by_id(mbedtls_pk_ec(k->pk)->MBEDTLS_PRIVATE(grp).id) : NULL; +} + +/* A parsed key is usable when it is RSA or EC on a curve above. */ +static bool sp_key_supported(const SpKey *k) { + return sp_is_ed(k) || sp_is_rsa(k) || (sp_is_ec(k) && sp_curve_of(k) != NULL); +} + +/* ── DER writing ──────────────────────────────────────────────────────── */ + +/* mbedtls writes DER backwards into the end of a buffer; these helpers + * answer the written length or a negative error. */ +#define SP_DER_MAX 8192 + +static int sp_write_private_der(SpKey *k, unsigned char *buf, size_t size) { + return mbedtls_pk_write_key_der(&k->pk, buf, size); +} + +/* Ed25519's PKCS#8 and SubjectPublicKeyInfo prefixes (RFC 8410): the + * 32-byte seed or public key follows. */ +static const uint8_t sp_ed_pkcs8_prefix[16] = {0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, + 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20}; +static const uint8_t sp_ed_spki_prefix[12] = {0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, + 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00}; + +/* Writes prefix || 32 bytes at the end of buf, as the mbedtls writers do. */ +static int sp_write_ed_der(const uint8_t *prefix, size_t prefix_len, const uint8_t key[32], + unsigned char *buf, size_t size) { + size_t n = prefix_len + 32; + if (size < n) return MBEDTLS_ERR_ASN1_BUF_TOO_SMALL; + memcpy(buf + size - n, prefix, prefix_len); + memcpy(buf + size - 32, key, 32); + return (int)n; +} + +static int sp_write_spki_der(SpKey *k, unsigned char *buf, size_t size) { + if (sp_is_ed(k)) return sp_write_ed_der(sp_ed_spki_prefix, sizeof sp_ed_spki_prefix, k->ed_pub, buf, size); + return mbedtls_pk_write_pubkey_der(&k->pk, buf, size); +} + +/* RSAPublicKey (PKCS#1) DER. */ +static int sp_write_pkcs1_public_der(SpKey *k, unsigned char *buf, size_t size) { + unsigned char *p = buf + size; + return mbedtls_pk_write_pubkey(&p, buf, &k->pk); +} + +/* PKCS#8 PrivateKeyInfo around the PKCS#1 or SEC1 key, as OpenSSL writes + * it: version 0, the AlgorithmIdentifier (rsaEncryption with NULL + * parameters, or id-ecPublicKey with the curve OID), and the inner key, + * which for EC omits the parameters field. */ +static int sp_write_pkcs8_der(SpKey *k, unsigned char *buf, size_t size) { + if (sp_is_ed(k)) + return sp_write_ed_der(sp_ed_pkcs8_prefix, sizeof sp_ed_pkcs8_prefix, k->ed_seed, buf, size); + unsigned char inner[SP_DER_MAX]; + size_t inner_len; + if (sp_is_rsa(k)) { + int n = sp_write_private_der(k, inner, sizeof inner); + if (n < 0) return n; + inner_len = (size_t)n; + memmove(inner, inner + sizeof inner - inner_len, inner_len); + } else { + /* ECPrivateKey ::= SEQUENCE { version 1, privateKey OCTET STRING, + * [1] publicKey BIT STRING } */ + const mbedtls_ecp_keypair *ec = mbedtls_pk_ec(k->pk); + unsigned char *c = inner + sizeof inner; + size_t len = 0; + int r; + unsigned char point[160]; + size_t point_len; + if ((r = mbedtls_ecp_point_write_binary(&ec->MBEDTLS_PRIVATE(grp), &ec->MBEDTLS_PRIVATE(Q), + MBEDTLS_ECP_PF_UNCOMPRESSED, &point_len, point, + sizeof point)) != 0) + return r; + size_t pub_len = 0; + if ((r = mbedtls_asn1_write_bitstring(&c, inner, point, point_len * 8)) < 0) return r; + pub_len += (size_t)r; + if ((r = mbedtls_asn1_write_len(&c, inner, pub_len)) < 0) return r; + pub_len += (size_t)r; + if ((r = mbedtls_asn1_write_tag(&c, inner, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 1)) < 0) + return r; + pub_len += (size_t)r; + len += pub_len; + size_t d_len = (ec->MBEDTLS_PRIVATE(grp).nbits + 7) / 8; + unsigned char d[72]; + if ((r = mbedtls_mpi_write_binary(&ec->MBEDTLS_PRIVATE(d), d, d_len)) != 0) return r; + if ((r = mbedtls_asn1_write_octet_string(&c, inner, d, d_len)) < 0) return r; + len += (size_t)r; + mbedtls_platform_zeroize(d, sizeof d); + if ((r = mbedtls_asn1_write_int(&c, inner, 1)) < 0) return r; + len += (size_t)r; + if ((r = mbedtls_asn1_write_len(&c, inner, len)) < 0) return r; + len += (size_t)r; + if ((r = mbedtls_asn1_write_tag(&c, inner, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) + return r; + len += (size_t)r; + inner_len = len; + memmove(inner, c, inner_len); + } + unsigned char *c = buf + size; + size_t len = 0; + int r; + if ((r = mbedtls_asn1_write_octet_string(&c, buf, inner, inner_len)) < 0) goto done; + len += (size_t)r; + { + const char *oid; + size_t oid_len; + size_t alg_len = 0; + if (sp_is_rsa(k)) { + if ((r = mbedtls_asn1_write_null(&c, buf)) < 0) goto done; + alg_len += (size_t)r; + oid = MBEDTLS_OID_PKCS1_RSA; + oid_len = MBEDTLS_OID_SIZE(MBEDTLS_OID_PKCS1_RSA); + } else { + const char *curve_oid; + size_t curve_oid_len; + if ((r = mbedtls_oid_get_oid_by_ec_grp(mbedtls_pk_ec(k->pk)->MBEDTLS_PRIVATE(grp).id, &curve_oid, + &curve_oid_len)) != 0) + goto done; + if ((r = mbedtls_asn1_write_oid(&c, buf, curve_oid, curve_oid_len)) < 0) goto done; + alg_len += (size_t)r; + oid = MBEDTLS_OID_EC_ALG_UNRESTRICTED; + oid_len = MBEDTLS_OID_SIZE(MBEDTLS_OID_EC_ALG_UNRESTRICTED); + } + if ((r = mbedtls_asn1_write_oid(&c, buf, oid, oid_len)) < 0) goto done; + alg_len += (size_t)r; + if ((r = mbedtls_asn1_write_len(&c, buf, alg_len)) < 0) goto done; + alg_len += (size_t)r; + if ((r = mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) + goto done; + alg_len += (size_t)r; + len += alg_len; + } + if ((r = mbedtls_asn1_write_int(&c, buf, 0)) < 0) goto done; + len += (size_t)r; + if ((r = mbedtls_asn1_write_len(&c, buf, len)) < 0) goto done; + len += (size_t)r; + if ((r = mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) < 0) + goto done; + len += (size_t)r; + r = (int)len; +done: + mbedtls_platform_zeroize(inner, sizeof inner); + return r; +} + +/* PEM armor with 64-character base64 lines and a final newline. */ +static ScrStr *sp_pem(const char *label, const unsigned char *der, size_t len) { + size_t b64_len = 0; + mbedtls_base64_encode(NULL, 0, &b64_len, der, len); + unsigned char *b64 = malloc(b64_len + 1); + char *out = malloc(b64_len + b64_len / 64 + 2 * strlen(label) + 64); + if (!b64 || !out) scr_trap("scriptc: out of memory\n"); + mbedtls_base64_encode(b64, b64_len + 1, &b64_len, der, len); + size_t o = (size_t)sprintf(out, "-----BEGIN %s-----\n", label); + for (size_t i = 0; i < b64_len; i += 64) { + size_t n = b64_len - i < 64 ? b64_len - i : 64; + memcpy(out + o, b64 + i, n); + o += n; + out[o++] = '\n'; + } + o += (size_t)sprintf(out + o, "-----END %s-----\n", label); + ScrStr *s = scr_str_new(out, o); + mbedtls_platform_zeroize(b64, b64_len); + mbedtls_platform_zeroize(out, o); + free(b64); + free(out); + return s; +} + +static ScrDyn *sp_dyn_buffer(const unsigned char *data, size_t len) { + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)len); + bytes->is_buffer = true; + if (len) memcpy(bytes->data, data, len); + ScrDyn *d = scr_dyn_new_buffer(bytes); + scr_bytes_release(bytes); + return d; +} + +static ScrDyn *sp_dyn_str(ScrStr *s) { + ScrDyn *d = scr_dyn_new_str(s); + scr_str_release(s); + return d; +} + +/* ── JWK ──────────────────────────────────────────────────────────────── */ + +/* base64url without padding of the big-endian integer `x` in `len` bytes + * (0 = its minimal length). */ +static ScrDyn *sp_jwk_int(const mbedtls_mpi *x, size_t len) { + if (len == 0) len = mbedtls_mpi_size(x); + if (len == 0) len = 1; + unsigned char *raw = malloc(len); + size_t b64_len = 0; + if (!raw) scr_trap("scriptc: out of memory\n"); + mbedtls_mpi_write_binary(x, raw, len); + mbedtls_base64_encode(NULL, 0, &b64_len, raw, len); + unsigned char *b64 = malloc(b64_len + 1); + if (!b64) scr_trap("scriptc: out of memory\n"); + mbedtls_base64_encode(b64, b64_len + 1, &b64_len, raw, len); + size_t o = 0; + for (size_t i = 0; i < b64_len; i++) { + unsigned char c = b64[i]; + if (c == '=') break; + b64[o++] = c == '+' ? '-' : c == '/' ? '_' : c; + } + ScrDyn *d = sp_dyn_str(scr_str_new((const char *)b64, o)); + mbedtls_platform_zeroize(raw, len); + free(raw); + free(b64); + return d; +} + +static void sp_set(ScrDyn *obj, const char *key, ScrDyn *value) { + scr_dyn_obj_set(obj, key, strlen(key), value); +} + +/* base64url without padding of raw bytes. */ +static ScrDyn *sp_b64url(const uint8_t *raw, size_t len) { + size_t b64_len = 0; + mbedtls_base64_encode(NULL, 0, &b64_len, raw, len); + unsigned char *b64 = malloc(b64_len + 1); + if (!b64) scr_trap("scriptc: out of memory\n"); + mbedtls_base64_encode(b64, b64_len + 1, &b64_len, raw, len); + size_t o = 0; + for (size_t i = 0; i < b64_len; i++) { + unsigned char c = b64[i]; + if (c == '=') break; + b64[o++] = c == '+' ? '-' : c == '/' ? '_' : c; + } + ScrDyn *d = sp_dyn_str(scr_str_new((const char *)b64, o)); + mbedtls_platform_zeroize(b64, b64_len); + free(b64); + return d; +} + +static ScrDyn *sp_export_jwk(SpKey *k) { + ScrDyn *jwk = scr_dyn_new_obj(); + if (sp_is_ed(k)) { + sp_set(jwk, "kty", sp_dyn_str(scr_str_new("OKP", 3))); + sp_set(jwk, "crv", sp_dyn_str(scr_str_new("Ed25519", 7))); + sp_set(jwk, "x", sp_b64url(k->ed_pub, 32)); + if (k->private_key) sp_set(jwk, "d", sp_b64url(k->ed_seed, 32)); + return jwk; + } + if (sp_is_rsa(k)) { + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(k->pk); + mbedtls_mpi n, e, d, p, q, dp, dq, qi; + mbedtls_mpi *all[] = {&n, &e, &d, &p, &q, &dp, &dq, &qi}; + for (size_t i = 0; i < 8; i++) mbedtls_mpi_init(all[i]); + mbedtls_rsa_export(rsa, &n, k->private_key ? &p : NULL, k->private_key ? &q : NULL, + k->private_key ? &d : NULL, &e); + sp_set(jwk, "kty", sp_dyn_str(scr_str_new("RSA", 3))); + sp_set(jwk, "n", sp_jwk_int(&n, 0)); + sp_set(jwk, "e", sp_jwk_int(&e, 0)); + if (k->private_key) { + mbedtls_rsa_export_crt(rsa, &dp, &dq, &qi); + sp_set(jwk, "d", sp_jwk_int(&d, 0)); + sp_set(jwk, "p", sp_jwk_int(&p, 0)); + sp_set(jwk, "q", sp_jwk_int(&q, 0)); + sp_set(jwk, "dp", sp_jwk_int(&dp, 0)); + sp_set(jwk, "dq", sp_jwk_int(&dq, 0)); + sp_set(jwk, "qi", sp_jwk_int(&qi, 0)); + } + for (size_t i = 0; i < 8; i++) mbedtls_mpi_free(all[i]); + } else { + const mbedtls_ecp_keypair *ec = mbedtls_pk_ec(k->pk); + const SpCurve *curve = sp_curve_of(k); + size_t size = (ec->MBEDTLS_PRIVATE(grp).nbits + 7) / 8; + sp_set(jwk, "kty", sp_dyn_str(scr_str_new("EC", 2))); + sp_set(jwk, "x", sp_jwk_int(&ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(X), size)); + sp_set(jwk, "y", sp_jwk_int(&ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Y), size)); + sp_set(jwk, "crv", sp_dyn_str(scr_str_new(curve->jwk, strlen(curve->jwk)))); + if (k->private_key) sp_set(jwk, "d", sp_jwk_int(&ec->MBEDTLS_PRIVATE(d), size)); + } + return jwk; +} + +/* Decodes a JWK base64url member into `x`; false when it is missing or + * not a string. */ +static bool sp_jwk_read(const ScrDyn *jwk, const char *key, mbedtls_mpi *x) { + const ScrDyn *v = sp_opt(jwk, key); + if (v->kind != SCR_DYN_STR) return false; + size_t n = v->v.str->len; + unsigned char *b64 = malloc(n + 4); + unsigned char *raw = malloc(n + 4); + if (!b64 || !raw) scr_trap("scriptc: out of memory\n"); + for (size_t i = 0; i < n; i++) { + char c = v->v.str->data[i]; + b64[i] = (unsigned char)(c == '-' ? '+' : c == '_' ? '/' : c); + } + while (n % 4) b64[n++] = '='; + size_t raw_len = 0; + bool ok = mbedtls_base64_decode(raw, n, &raw_len, b64, n) == 0 && + mbedtls_mpi_read_binary(x, raw, raw_len) == 0; + mbedtls_platform_zeroize(raw, n); + free(b64); + free(raw); + return ok; +} + +/* Decodes a 32-byte JWK base64url member; false when it is missing, not a + * string, or another length. */ +static bool sp_jwk_bytes(const ScrDyn *jwk, const char *key, uint8_t out[32]) { + const ScrDyn *v = sp_opt(jwk, key); + if (v->kind != SCR_DYN_STR || v->v.str->len != 43) return false; + unsigned char b64[44]; + for (size_t i = 0; i < 43; i++) { + char c = v->v.str->data[i]; + b64[i] = (unsigned char)(c == '-' ? '+' : c == '_' ? '/' : c); + } + b64[43] = '='; + size_t n = 0; + return mbedtls_base64_decode(out, 32, &n, b64, 44) == 0 && n == 32; +} + +static void sp_invalid_jwk(void) { + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_JWK", "Invalid JWK data"); +} + +/* A key from a JWK object: RSA (n, e, and d, p, q for a private key) or EC + * (crv, x, y, and d). NULL with the error pending. */ +static SpKey *sp_key_from_jwk_plain(const ScrDyn *jwk, bool want_private); + +static SpKey *sp_key_from_jwk(const ScrDyn *jwk, bool want_private) { + ScrDyn *plain = sp_plain(jwk); + if (!plain) return NULL; + SpKey *k = sp_key_from_jwk_plain(plain, want_private); + scr_dyn_release(plain); + return k; +} + +static SpKey *sp_key_from_jwk_plain(const ScrDyn *jwk, bool want_private) { + if (!sp_is_object(jwk)) { + scr_dyn_prop_type_fail("key.key", "of type object", jwk); + return NULL; + } + const ScrDyn *kty = sp_opt(jwk, "kty"); + if (kty->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("key.kty", "of type string", kty); + return NULL; + } + bool has_d = !sp_absent(sp_opt(jwk, "d")); + if (want_private && !has_d) { + sp_invalid_jwk(); + return NULL; + } + bool private_key = want_private; + SpKey *k = sp_key_alloc(private_key); + bool ok = false; + if (sp_is_str(kty, "OKP")) { + const ScrDyn *crv = sp_opt(jwk, "crv"); + if (!sp_is_str(crv, "Ed25519")) { + sp_key_release(k); + if (crv->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("key.crv", "of type string", crv); + return NULL; + } + sp_not_supported("JWK keys of this crv"); + return NULL; + } + k->ed25519 = true; + if (private_key) { + ok = sp_jwk_bytes(jwk, "d", k->ed_seed); + if (ok) { + const ScrPkiEd25519 *ed = sp_ed(); + if (!ed) { + sp_key_release(k); + return NULL; + } + ed->public_key(k->ed_pub, k->ed_seed); + } + } else { + ok = sp_jwk_bytes(jwk, "x", k->ed_pub); + } + } else if (sp_is_str(kty, "RSA")) { + mbedtls_mpi n, e, d, p, q; + mbedtls_mpi_init(&n); + mbedtls_mpi_init(&e); + mbedtls_mpi_init(&d); + mbedtls_mpi_init(&p); + mbedtls_mpi_init(&q); + if (sp_jwk_read(jwk, "n", &n) && sp_jwk_read(jwk, "e", &e) && + mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) == 0) { + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(k->pk); + if (private_key) { + ok = sp_jwk_read(jwk, "d", &d) && sp_jwk_read(jwk, "p", &p) && sp_jwk_read(jwk, "q", &q) && + mbedtls_rsa_import(rsa, &n, &p, &q, &d, &e) == 0 && mbedtls_rsa_complete(rsa) == 0 && + mbedtls_rsa_check_privkey(rsa) == 0; + } else { + ok = mbedtls_rsa_import(rsa, &n, NULL, NULL, NULL, &e) == 0 && + mbedtls_rsa_complete(rsa) == 0 && mbedtls_rsa_check_pubkey(rsa) == 0; + } + } + mbedtls_mpi_free(&n); + mbedtls_mpi_free(&e); + mbedtls_mpi_free(&d); + mbedtls_mpi_free(&p); + mbedtls_mpi_free(&q); + } else if (sp_is_str(kty, "EC")) { + const ScrDyn *crv = sp_opt(jwk, "crv"); + if (crv->kind != SCR_DYN_STR) { + sp_key_release(k); + scr_dyn_prop_type_fail("key.crv", "of type string", crv); + return NULL; + } + const SpCurve *curve = NULL; + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (sp_is_str(crv, sp_curves[i].jwk)) curve = &sp_curves[i]; + if (!curve) { + sp_key_release(k); + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_CURVE", "Invalid EC curve name"); + return NULL; + } + if (mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) == 0) { + mbedtls_ecp_keypair *ec = mbedtls_pk_ec(k->pk); + mbedtls_ecp_point *Q = &ec->MBEDTLS_PRIVATE(Q); + ok = mbedtls_ecp_group_load(&ec->MBEDTLS_PRIVATE(grp), curve->id) == 0 && + sp_jwk_read(jwk, "x", &Q->MBEDTLS_PRIVATE(X)) && + sp_jwk_read(jwk, "y", &Q->MBEDTLS_PRIVATE(Y)) && + mbedtls_mpi_lset(&Q->MBEDTLS_PRIVATE(Z), 1) == 0 && + mbedtls_ecp_check_pubkey(&ec->MBEDTLS_PRIVATE(grp), Q) == 0; + if (ok && private_key) + ok = sp_jwk_read(jwk, "d", &ec->MBEDTLS_PRIVATE(d)) && + mbedtls_ecp_check_privkey(&ec->MBEDTLS_PRIVATE(grp), &ec->MBEDTLS_PRIVATE(d)) == 0; + } + } else { + sp_key_release(k); + sp_not_supported("JWK keys of this kty"); + return NULL; + } + if (!ok) { + sp_key_release(k); + sp_invalid_jwk(); + return NULL; + } + return k; +} + +/* ── parsing ──────────────────────────────────────────────────────────── */ + +/* The private key of a public SpKey derived from `k`. */ +static SpKey *sp_public_of(SpKey *k) { + if (sp_is_ed(k)) { + SpKey *pub = sp_key_alloc(false); + pub->ed25519 = true; + memcpy(pub->ed_pub, k->ed_pub, 32); + return pub; + } + unsigned char der[SP_DER_MAX]; + int n = sp_write_spki_der(k, der, sizeof der); + SpKey *pub = sp_key_alloc(false); + if (n < 0 || mbedtls_pk_parse_public_key(&pub->pk, der + sizeof der - n, (size_t)n) != 0) { + sp_key_release(pub); + return NULL; + } + return pub; +} + +/* Parses key material (PEM or DER bytes). `private_key` selects + * createPrivateKey's parse (PKCS#1, SEC1, PKCS#8) or createPublicKey's + * (SubjectPublicKeyInfo, PKCS#1 RSAPublicKey, then a private key or an + * X.509 certificate, whose public key it takes). NULL with Node's + * decoder error pending. */ +/* The DER of the first PEM block in data (+1 malloc'd in *out), or the + * data itself when it has no PEM armor (*out NULL). */ +static bool sp_der_of(const unsigned char *data, size_t len, unsigned char **out, const unsigned char **der, + size_t *der_len) { + *out = NULL; + const unsigned char *begin = NULL; + for (size_t i = 0; i + 11 <= len; i++) { + if (memcmp(data + i, "-----BEGIN ", 11) == 0) { + begin = data + i; + break; + } + } + if (!begin) { + *der = data; + *der_len = len; + return true; + } + const unsigned char *end = data + len; + const unsigned char *body = memchr(begin, '\n', (size_t)(end - begin)); + if (!body) return false; + body++; + unsigned char *b64 = malloc((size_t)(end - body) + 1); + unsigned char *raw = malloc((size_t)(end - body) + 1); + if (!b64 || !raw) scr_trap("scriptc: out of memory\n"); + size_t n = 0; + for (const unsigned char *p = body; p < end && *p != '-'; p++) + if (*p != '\n' && *p != '\r' && *p != ' ' && *p != '\t') b64[n++] = *p; + size_t raw_len = 0; + bool ok = mbedtls_base64_decode(raw, (size_t)(end - body) + 1, &raw_len, b64, n) == 0; + free(b64); + if (!ok) { + free(raw); + return false; + } + *out = raw; + *der = raw; + *der_len = raw_len; + return true; +} + +/* The SubjectPublicKeyInfo element of an X.509 certificate's DER. */ +static bool sp_cert_spki(const unsigned char *der, size_t len, const unsigned char **spki, size_t *spki_len) { + unsigned char *p = (unsigned char *)der; + const unsigned char *end = der + len; + size_t n; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) return false; + if (mbedtls_asn1_get_tag(&p, end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) return false; + const unsigned char *tbs_end = p + n; + /* [0] version, serialNumber, signature, issuer, validity, subject */ + if (mbedtls_asn1_get_tag(&p, tbs_end, &n, MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 0) == 0) + p += n; + int tags[] = {MBEDTLS_ASN1_INTEGER, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE, + MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE, + MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE}; + for (size_t i = 0; i < 5; i++) { + if (mbedtls_asn1_get_tag(&p, tbs_end, &n, tags[i]) != 0) return false; + p += n; + } + const unsigned char *start = p; + if (mbedtls_asn1_get_tag(&p, tbs_end, &n, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) != 0) return false; + *spki = start; + *spki_len = (size_t)(p + n - start); + return true; +} + +/* An Ed25519 key from PKCS#8 or SubjectPublicKeyInfo DER, or (for a public + * key) the key of an X.509 certificate; NULL when the DER is none of + * these, or with *failed set and an error pending. A private key wanted + * as public answers its public key. */ +static SpKey *sp_ed_from_der(const unsigned char *der, size_t len, bool private_key, bool *failed) { + *failed = false; + if (len == 48 && memcmp(der, sp_ed_pkcs8_prefix, 16) == 0) { + const ScrPkiEd25519 *ed = sp_ed(); + if (!ed) { + *failed = true; + return NULL; + } + SpKey *k = sp_key_alloc(private_key); + k->ed25519 = true; + ed->public_key(k->ed_pub, der + 16); + if (private_key) memcpy(k->ed_seed, der + 16, 32); + return k; + } + if (private_key) return NULL; + const unsigned char *spki = der; + size_t spki_len = len; + if (!(len == 44 && memcmp(der, sp_ed_spki_prefix, 12) == 0) && !sp_cert_spki(der, len, &spki, &spki_len)) + return NULL; + if (spki_len != 44 || memcmp(spki, sp_ed_spki_prefix, 12) != 0) return NULL; + SpKey *k = sp_key_alloc(false); + k->ed25519 = true; + memcpy(k->ed_pub, spki + 12, 32); + return k; +} + +static SpKey *sp_parse(const unsigned char *data, size_t len, bool private_key) { + { + unsigned char *decoded; + const unsigned char *der; + size_t der_len; + if (sp_der_of(data, len, &decoded, &der, &der_len)) { + bool failed; + SpKey *ed = sp_ed_from_der(der, der_len, private_key, &failed); + if (decoded) { + mbedtls_platform_zeroize(decoded, der_len); + free(decoded); + } + if (ed || failed) return ed; + } + } + /* mbedtls reads PEM from a NUL-terminated buffer whose length counts the + * NUL. */ + bool pem = false; + for (size_t i = 0; i + 11 <= len; i++) { + if (memcmp(data + i, "-----BEGIN ", 11) == 0) { + pem = true; + break; + } + } + unsigned char *buf = malloc(len + 1); + if (!buf) scr_trap("scriptc: out of memory\n"); + memcpy(buf, data, len); + buf[len] = 0; + size_t n = pem ? len + 1 : len; + SpKey *k = sp_key_alloc(private_key); + bool ok; + if (private_key) { + ok = mbedtls_pk_parse_key(&k->pk, buf, n, NULL, 0, sp_random, NULL) == 0; + } else { + ok = mbedtls_pk_parse_public_key(&k->pk, buf, n) == 0; + if (!ok) { + SpKey *priv = sp_key_alloc(true); + mbedtls_pk_free(&k->pk); + mbedtls_pk_init(&k->pk); + if (mbedtls_pk_parse_key(&priv->pk, buf, n, NULL, 0, sp_random, NULL) == 0) { + sp_key_release(k); + k = sp_public_of(priv); + ok = k != NULL; + if (!k) k = sp_key_alloc(false); + } else { + mbedtls_x509_crt crt; + mbedtls_x509_crt_init(&crt); + if (mbedtls_x509_crt_parse(&crt, buf, n) == 0) { + unsigned char der[SP_DER_MAX]; + int w = mbedtls_pk_write_pubkey_der(&crt.pk, der, sizeof der); + ok = w > 0 && mbedtls_pk_parse_public_key(&k->pk, der + sizeof der - w, (size_t)w) == 0; + } + mbedtls_x509_crt_free(&crt); + } + sp_key_release(priv); + } + } + mbedtls_platform_zeroize(buf, len); + free(buf); + if (!ok) { + sp_key_release(k); + sp_unsupported_input(); + return NULL; + } + if (!sp_key_supported(k)) { + sp_key_release(k); + sp_not_supported("crypto keys of this type"); + return NULL; + } + return k; +} + +/* The bytes of a string or buffer source, +1; NULL when `v` is neither. */ +static ScrBytes *sp_source_bytes(const ScrDyn *v, const ScrDyn *encoding) { + if (v->kind == SCR_DYN_STR) { + bool named = encoding != NULL && encoding->kind == SCR_DYN_STR; + ScrStr *utf8 = named ? NULL : scr_str_new("utf8", 4); + ScrBytes *b = scr_bytes_from_str_checked(v->v.str, named ? encoding->v.str : utf8); + if (utf8) scr_str_release(utf8); + return b; + } + if (v->kind == SCR_DYN_BYTES || scr_buffer_storage_is(v)) return scr_bytes_buffer_source(v); + return NULL; +} + +static const char sp_key_input_types[] = + "of type string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, or URL"; + +/* createPrivateKey / createPublicKey: a PEM string or buffer, or + * { key, format, type, encoding, passphrase }; createPublicKey also takes + * a private KeyObject. */ +static ScrDyn *sp_create_key(ScrDyn *input, bool private_key) { + if (input->kind == SCR_DYN_TYPED_REF) { + ScrDyn *value = scr_dyn_typed_ref_materialize(input); + if (!value) return NULL; + ScrDyn *result = sp_create_key(value, private_key); + scr_dyn_release(value); + return result; + } + SpKey *object = sp_key_of(input); + if (object) { + if (private_key) { + scr_dyn_arg_type_fail("key", sp_key_input_types, input); + return NULL; + } + if (!object->private_key) { + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE", + "Invalid key object type public, expected private."); + return NULL; + } + SpKey *pub = sp_public_of(object); + if (!pub) { + sp_unsupported_input(); + return NULL; + } + return sp_key_box(pub); + } + const ScrDyn *data = input; + const ScrDyn *format = scr_dyn_undefined(); + const ScrDyn *type = scr_dyn_undefined(); + const ScrDyn *encoding = NULL; + if (sp_is_object(input) && !scr_buffer_storage_is(input)) { + data = sp_opt(input, "key"); + format = sp_opt(input, "format"); + type = sp_opt(input, "type"); + encoding = sp_opt(input, "encoding"); + if (!sp_absent(sp_opt(input, "passphrase"))) { + sp_not_supported("encrypted private keys"); + return NULL; + } + SpKey *inner = sp_key_of(data); + if (inner) return sp_create_key((ScrDyn *)data, private_key); + if (sp_is_str(format, "jwk")) { + SpKey *k = sp_key_from_jwk(data, private_key); + return k ? sp_key_box(k) : NULL; + } + if (!sp_absent(format) && !sp_is_str(format, "pem") && !sp_is_str(format, "der")) { + sp_property_invalid("key.format", format); + return NULL; + } + if (sp_is_str(format, "der")) { + bool valid = private_key ? sp_is_str(type, "pkcs1") || sp_is_str(type, "pkcs8") || + sp_is_str(type, "sec1") + : sp_is_str(type, "pkcs1") || sp_is_str(type, "spki"); + if (!valid) { + sp_property_invalid("key.type", type); + return NULL; + } + } + } + ScrBytes *bytes = sp_source_bytes(data, encoding); + if (!bytes) { + if (data->kind == SCR_DYN_STR || data->kind == SCR_DYN_BYTES || scr_buffer_storage_is(data)) + return NULL; /* the decoder threw */ + scr_dyn_arg_type_fail(data == input ? "key" : "key.key", sp_key_input_types, data); + return NULL; + } + SpKey *k = sp_parse(bytes->data, (size_t)scr_bytes_byte_len(bytes), private_key); + scr_bytes_release(bytes); + return k ? sp_key_box(k) : NULL; +} + +ScrDyn *scr_crypto_create_private_key(ScrDyn *key) { return sp_create_key(key, true); } + +ScrDyn *scr_crypto_create_public_key(ScrDyn *key) { return sp_create_key(key, false); } + +/* ── export ───────────────────────────────────────────────────────────── */ + +/* export({ format, type }) per Node's parseKeyEncoding: format "pem", + * "der" or "jwk"; type pkcs1/pkcs8/sec1 for private keys and pkcs1/spki + * for public keys, where pkcs1 needs an RSA key and sec1 an EC key. */ +static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options); + +static ScrDyn *sp_export(SpKey *k, const ScrDyn *options) { + ScrDyn *plain = sp_plain(options); + if (!plain) return NULL; + ScrDyn *result = sp_export_plain(k, plain); + scr_dyn_release(plain); + return result; +} + +static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { + if (options == NULL || !sp_is_object(options)) { + scr_dyn_arg_type_fail("options", "of type object", options ? options : scr_dyn_undefined()); + return NULL; + } + const ScrDyn *format = sp_opt(options, "format"); + if (sp_is_str(format, "jwk")) return sp_export_jwk(k); + bool pem = sp_is_str(format, "pem"); + if (!pem && !sp_is_str(format, "der")) { + sp_property_invalid("options.format", format); + return NULL; + } + const ScrDyn *type = sp_opt(options, "type"); + bool pkcs1 = sp_is_str(type, "pkcs1"); + bool pkcs8 = k->private_key && sp_is_str(type, "pkcs8"); + bool sec1 = k->private_key && sp_is_str(type, "sec1"); + bool spki = !k->private_key && sp_is_str(type, "spki"); + if (!pkcs1 && !pkcs8 && !sec1 && !spki) { + sp_property_invalid("options.type", type); + return NULL; + } + if (!sp_absent(sp_opt(options, "cipher")) || !sp_absent(sp_opt(options, "passphrase"))) { + sp_not_supported("encrypted private key export"); + return NULL; + } + if (pkcs1 && !sp_is_rsa(k)) { + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", + "The selected key encoding pkcs1 can only be used for RSA keys."); + return NULL; + } + if (sec1 && !sp_is_ec(k)) { + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", + "The selected key encoding sec1 can only be used for EC keys."); + return NULL; + } + unsigned char *der = malloc(SP_DER_MAX); + if (!der) scr_trap("scriptc: out of memory\n"); + int n; + const char *label; + if (pkcs8) { + n = sp_write_pkcs8_der(k, der, SP_DER_MAX); + label = "PRIVATE KEY"; + } else if (spki) { + n = sp_write_spki_der(k, der, SP_DER_MAX); + label = "PUBLIC KEY"; + } else if (k->private_key) { + n = sp_write_private_der(k, der, SP_DER_MAX); + label = pkcs1 ? "RSA PRIVATE KEY" : "EC PRIVATE KEY"; + } else { + n = sp_write_pkcs1_public_der(k, der, SP_DER_MAX); + label = "RSA PUBLIC KEY"; + } + ScrDyn *result = NULL; + if (n < 0) { + sp_unsupported_input(); + } else { + const unsigned char *start = der + SP_DER_MAX - n; + result = pem ? sp_dyn_str(sp_pem(label, start, (size_t)n)) : sp_dyn_buffer(start, (size_t)n); + } + mbedtls_platform_zeroize(der, SP_DER_MAX); + free(der); + return result; +} + +/* ── the KeyObject handles ────────────────────────────────────────────── */ + +static ScrDyn *sp_key_details(SpKey *k) { + ScrDyn *details = scr_dyn_new_obj(); + if (sp_is_ed(k)) return details; + if (sp_is_rsa(k)) { + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(k->pk); + mbedtls_mpi e; + mbedtls_mpi_init(&e); + mbedtls_rsa_export(rsa, NULL, NULL, NULL, NULL, &e); + uint64_t exponent = 0; + size_t bytes = mbedtls_mpi_size(&e); + unsigned char raw[8] = {0}; + if (bytes <= 8) mbedtls_mpi_write_binary(&e, raw, 8); + for (size_t i = 0; i < 8; i++) exponent = (exponent << 8) | raw[i]; + mbedtls_mpi_free(&e); + sp_set(details, "modulusLength", scr_dyn_new_num((double)mbedtls_pk_get_bitlen(&k->pk))); + ScrBigInt *big = scr_bigint_from_u64(exponent); + sp_set(details, "publicExponent", scr_dyn_new_bigint(big)); + scr_bigint_release(big); + } else { + const SpCurve *curve = sp_curve_of(k); + sp_set(details, "namedCurve", sp_dyn_str(scr_str_new(curve->name, strlen(curve->name)))); + } + return details; +} + +/* The DER that identifies a key for equals(): its private encoding for a + * private key, its SubjectPublicKeyInfo otherwise. */ +static int sp_identity_der(SpKey *k, unsigned char *buf, size_t size) { + if (sp_is_ed(k)) return k->private_key ? sp_write_pkcs8_der(k, buf, size) : sp_write_spki_der(k, buf, size); + return k->private_key ? sp_write_private_der(k, buf, size) : sp_write_spki_der(k, buf, size); +} + +static ScrDyn *sp_key_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + (void)self; + SpKey *k = h; + if (strcmp(method, "export") == 0) return sp_export(k, argc ? args[0] : NULL); + if (strcmp(method, "equals") == 0) { + const ScrDyn *other_value = argc ? args[0] : scr_dyn_undefined(); + SpKey *other = sp_key_of(other_value); + if (!other) { + size_t len; + if (scr_key_object_bytes(other_value, &len)) return scr_dyn_new_bool(false); + scr_dyn_arg_type_fail("otherKeyObject", "an instance of KeyObject", other_value); + return NULL; + } + if (other->private_key != k->private_key || + mbedtls_pk_get_type(&other->pk) != mbedtls_pk_get_type(&k->pk)) + return scr_dyn_new_bool(false); + unsigned char *a = malloc(SP_DER_MAX), *b = malloc(SP_DER_MAX); + if (!a || !b) scr_trap("scriptc: out of memory\n"); + int na = sp_identity_der(k, a, SP_DER_MAX); + int nb = sp_identity_der(other, b, SP_DER_MAX); + bool same = na > 0 && na == nb && memcmp(a + SP_DER_MAX - na, b + SP_DER_MAX - nb, (size_t)na) == 0; + mbedtls_platform_zeroize(a, SP_DER_MAX); + mbedtls_platform_zeroize(b, SP_DER_MAX); + free(a); + free(b); + return scr_dyn_new_bool(same); + } + if (strcmp(method, "toCryptoKey") == 0) { + sp_not_supported("KeyObject.toCryptoKey"); + return NULL; + } + char message[160]; + int n = snprintf(message, sizeof message, "%s is not a function", what); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + return NULL; +} + +static ScrDyn *sp_key_get(void *h, const char *name, size_t len) { + SpKey *k = h; + if (len == 4 && memcmp(name, "type", 4) == 0) + return k->private_key ? sp_dyn_str(scr_str_new("private", 7)) : sp_dyn_str(scr_str_new("public", 6)); + if (len == 17 && memcmp(name, "asymmetricKeyType", 17) == 0) + return sp_is_ed(k) ? sp_dyn_str(scr_str_new("ed25519", 7)) + : sp_is_rsa(k) ? sp_dyn_str(scr_str_new("rsa", 3)) + : sp_dyn_str(scr_str_new("ec", 2)); + if (len == 20 && memcmp(name, "asymmetricKeyDetails", 20) == 0) return sp_key_details(k); + return NULL; +} + +static bool sp_key_set(void *h, const char *name, size_t len, const ScrDyn *value) { + (void)h; + (void)value; + const char *holder = len == 4 && memcmp(name, "type", 4) == 0 ? "KeyObject" + : (len == 17 && memcmp(name, "asymmetricKeyType", 17) == 0) || + (len == 20 && memcmp(name, "asymmetricKeyDetails", 20) == 0) + ? "AsymmetricKeyObject" + : NULL; + if (!holder) return false; + char message[160]; + int n = snprintf(message, sizeof message, "Cannot set property %.*s of #<%s> which has only a getter", + (int)len, name, holder); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + return true; +} + +static ScrDyn *sp_key_inspect_view(void *h) { + (void)h; + return scr_dyn_new_obj(); +} + +static const ScrDynHandleOps sp_private_ops = { + .cls = "PrivateKeyObject", + .retain = sp_key_retain, + .release = sp_key_release, + .invoke = sp_key_invoke, + .get = sp_key_get, + .set = sp_key_set, + .inspect_view = sp_key_inspect_view, + .json_empty_object = true, + .to_string_tag = "KeyObject", + .inspect_view_empty = true, +}; + +static const ScrDynHandleOps sp_public_ops = { + .cls = "PublicKeyObject", + .retain = sp_key_retain, + .release = sp_key_release, + .invoke = sp_key_invoke, + .get = sp_key_get, + .set = sp_key_set, + .inspect_view = sp_key_inspect_view, + .json_empty_object = true, + .to_string_tag = "KeyObject", + .inspect_view_empty = true, +}; + +/* createHmac with a private or public key (scr_crypto_reject_asymmetric_secret). */ +static void sp_reject_as_secret(const ScrDyn *key) { + bool private_key = key->v.handle.tag == SCR_DYNH_PRIVATE_KEY; + scr_throw_error_msg_code(SCR_ERR_TYPE, + private_key ? "Invalid key object type private, expected secret." + : "Invalid key object type public, expected secret.", + private_key ? 49 : 48, "ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE"); +} + +static void sp_key_install(void) { + static SCR_TL bool installed; + if (installed) return; + installed = true; + scr_dyn_handle_install(SCR_DYNH_PRIVATE_KEY, &sp_private_ops); + scr_dyn_handle_install(SCR_DYNH_PUBLIC_KEY, &sp_public_ops); + scr_crypto_reject_asymmetric_secret = sp_reject_as_secret; +} + +/* ── generateKeyPairSync ──────────────────────────────────────────────── */ + +/* A key in the encoding an options.publicKeyEncoding/privateKeyEncoding + * object asks for, or the KeyObject when it is undefined. Consumes `k`. */ +static ScrDyn *sp_encoded(SpKey *k, const ScrDyn *encoding) { + if (sp_absent(encoding)) return sp_key_box(k); + ScrDyn *result = sp_export(k, encoding); + sp_key_release(k); + return result; +} + +static bool sp_check_encoding_plain(const ScrDyn *encoding, const char *name, bool private_key); + +static bool sp_check_encoding(const ScrDyn *options, const char *name, bool private_key) { + ScrDyn *encoding = sp_plain(sp_opt(options, name)); + if (!encoding) return false; + bool ok = sp_check_encoding_plain(encoding, name, private_key); + scr_dyn_release(encoding); + return ok; +} + +static bool sp_check_encoding_plain(const ScrDyn *encoding, const char *name, bool private_key) { + if (sp_absent(encoding)) return true; + char full[64]; + snprintf(full, sizeof full, "options.%s", name); + if (!sp_is_object(encoding)) { + sp_property_invalid(full, encoding); + return false; + } + const ScrDyn *format = sp_opt(encoding, "format"); + if (!sp_is_str(format, "pem") && !sp_is_str(format, "der") && !sp_is_str(format, "jwk")) { + snprintf(full, sizeof full, "options.%s.format", name); + sp_property_invalid(full, format); + return false; + } + if (sp_is_str(format, "jwk")) return true; + const ScrDyn *type = sp_opt(encoding, "type"); + bool valid = private_key ? sp_is_str(type, "pkcs1") || sp_is_str(type, "pkcs8") || sp_is_str(type, "sec1") + : sp_is_str(type, "pkcs1") || sp_is_str(type, "spki"); + if (!valid) { + snprintf(full, sizeof full, "options.%s.type", name); + sp_property_invalid(full, type); + return false; + } + return true; +} + +/* Generates the private key for (type, options), validated in Node's + * order; NULL with the error pending. */ +static SpKey *sp_generate(const ScrDyn *type, const ScrDyn *options) { + if (type->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("type", "of type string", type); + return NULL; + } + bool rsa = sp_is_str(type, "rsa"); + bool ec = sp_is_str(type, "ec"); + bool ed = sp_is_str(type, "ed25519"); + static const char *const known[] = {"rsa-pss", "dsa", "ed448", "x25519", "x448", "dh"}; + bool other = false; + for (size_t i = 0; i < sizeof known / sizeof known[0]; i++) other = other || sp_is_str(type, known[i]); + if (!rsa && !ec && !ed && !other) { + char lite[96]; + char message[200]; + int n = snprintf(message, sizeof message, + "The argument 'type' must be a supported key type. Received %s", + scr_dyn_inspect_lite(type, lite, sizeof lite)); + scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)n, "ERR_INVALID_ARG_VALUE"); + return NULL; + } + /* The ed25519 options are optional. */ + if (!sp_is_object(options) && !(ed && sp_absent(options))) { + scr_dyn_arg_type_fail("options", "of type object", options); + return NULL; + } + if (other) { + sp_not_supported("generateKeyPairSync for this key type"); + return NULL; + } + SpKey *k = sp_key_alloc(true); + if (ed) { + if (!sp_check_encoding(options, "publicKeyEncoding", false) || + !sp_check_encoding(options, "privateKeyEncoding", true)) { + sp_key_release(k); + return NULL; + } + const ScrPkiEd25519 *ed = sp_ed(); + if (!ed) { + sp_key_release(k); + return NULL; + } + k->ed25519 = true; + sp_random(NULL, k->ed_seed, 32); + ed->public_key(k->ed_pub, k->ed_seed); + return k; + } + if (rsa) { + const ScrDyn *bits = sp_opt(options, "modulusLength"); + if (bits->kind != SCR_DYN_NUM) { + sp_key_release(k); + scr_dyn_prop_type_fail("options.modulusLength", "of type number", bits); + return NULL; + } + double b = bits->v.num; + if (b != b || b < 0 || b > 4294967295.0 || b != (double)(uint64_t)b) { + sp_key_release(k); + char message[160]; + char lite[64]; + int n = snprintf(message, sizeof message, + "The value of \"options.modulusLength\" is out of range. It must be >= 0 && <= 4294967295. Received %s", + scr_dyn_inspect_lite(bits, lite, sizeof lite)); + scr_throw_error_msg_code(SCR_ERR_RANGE, message, (size_t)n, "ERR_OUT_OF_RANGE"); + return NULL; + } + const ScrDyn *exp = sp_opt(options, "publicExponent"); + double e = 65537; + if (!sp_absent(exp)) { + if (exp->kind != SCR_DYN_NUM) { + sp_key_release(k); + scr_dyn_prop_type_fail("options.publicExponent", "of type number", exp); + return NULL; + } + e = exp->v.num; + } + if (!sp_check_encoding(options, "publicKeyEncoding", false) || + !sp_check_encoding(options, "privateKeyEncoding", true)) { + sp_key_release(k); + return NULL; + } + if (b < 512 || b > 16384 || e < 3 || e > 2147483647.0 || e != (double)(int64_t)e || ((int64_t)e & 1) == 0 || + mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0 || + mbedtls_rsa_gen_key(mbedtls_pk_rsa(k->pk), sp_random, NULL, (unsigned)b, (int)e) != 0) { + sp_key_release(k); + sp_not_supported("generateKeyPairSync with this RSA modulus length or exponent"); + return NULL; + } + return k; + } + const ScrDyn *curve_name = sp_opt(options, "namedCurve"); + if (curve_name->kind != SCR_DYN_STR) { + sp_key_release(k); + scr_dyn_prop_type_fail("options.namedCurve", "of type string", curve_name); + return NULL; + } + if (!sp_check_encoding(options, "publicKeyEncoding", false) || + !sp_check_encoding(options, "privateKeyEncoding", true)) { + sp_key_release(k); + return NULL; + } + const SpCurve *curve = NULL; + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (sp_is_str(curve_name, sp_curves[i].name) || + (sp_curves[i].nist && sp_is_str(curve_name, sp_curves[i].nist))) + curve = &sp_curves[i]; + if (!curve) { + sp_key_release(k); + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_CURVE", "Invalid EC curve name"); + return NULL; + } + if (mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) != 0 || + mbedtls_ecp_gen_key(curve->id, mbedtls_pk_ec(k->pk), sp_random, NULL) != 0) { + sp_key_release(k); + sp_unsupported_input(); + return NULL; + } + return k; +} + +/* generateKeyPairSync(type, options): { publicKey, privateKey }, each a + * KeyObject or the encoding the options ask for. */ +ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options) { + if (options->kind == SCR_DYN_TYPED_REF) { + ScrDyn *value = scr_dyn_typed_ref_materialize(options); + if (!value) return NULL; + ScrDyn *result = scr_crypto_generate_key_pair_sync(type, value); + scr_dyn_release(value); + return result; + } + SpKey *k = sp_generate(type, options); + if (!k) return NULL; + SpKey *pub = sp_public_of(k); + if (!pub) { + sp_key_release(k); + sp_unsupported_input(); + return NULL; + } + ScrDyn *public_value = sp_encoded(pub, sp_opt(options, "publicKeyEncoding")); + if (!public_value) { + sp_key_release(k); + return NULL; + } + ScrDyn *private_value = sp_encoded(k, sp_opt(options, "privateKeyEncoding")); + if (!private_value) { + scr_dyn_release(public_value); + return NULL; + } + ScrDyn *pair = scr_dyn_new_obj(); + sp_set(pair, "publicKey", public_value); + sp_set(pair, "privateKey", private_value); + return pair; +} + +/* ── sign and verify ──────────────────────────────────────────────────── */ + +typedef struct { + const char *name; + mbedtls_md_type_t md; +} SpDigest; + +/* Digest names as OpenSSL's EVP_get_digestbyname takes them for + * signatures, matched without regard to case. */ +static const SpDigest sp_digests[] = { + {"sha1", MBEDTLS_MD_SHA1}, {"rsa-sha1", MBEDTLS_MD_SHA1}, + {"sha1withrsaencryption", MBEDTLS_MD_SHA1}, {"ecdsa-with-sha1", MBEDTLS_MD_SHA1}, + {"sha224", MBEDTLS_MD_SHA224}, {"rsa-sha224", MBEDTLS_MD_SHA224}, + {"sha224withrsaencryption", MBEDTLS_MD_SHA224}, + {"sha256", MBEDTLS_MD_SHA256}, {"rsa-sha256", MBEDTLS_MD_SHA256}, + {"sha256withrsaencryption", MBEDTLS_MD_SHA256}, + {"sha384", MBEDTLS_MD_SHA384}, {"rsa-sha384", MBEDTLS_MD_SHA384}, + {"sha384withrsaencryption", MBEDTLS_MD_SHA384}, + {"sha512", MBEDTLS_MD_SHA512}, {"rsa-sha512", MBEDTLS_MD_SHA512}, + {"sha512withrsaencryption", MBEDTLS_MD_SHA512}, + {"sha2-256", MBEDTLS_MD_SHA256}, {"sha2-384", MBEDTLS_MD_SHA384}, + {"sha2-512", MBEDTLS_MD_SHA512}, {"sha2-224", MBEDTLS_MD_SHA224}, +}; + +static bool sp_digest_of(const ScrStr *name, mbedtls_md_type_t *md) { + for (size_t i = 0; i < sizeof sp_digests / sizeof sp_digests[0]; i++) { + size_t n = strlen(sp_digests[i].name); + if (name->len != n) continue; + bool same = true; + for (size_t j = 0; j < n && same; j++) { + char c = name->data[j]; + if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); + same = c == sp_digests[i].name[j]; + } + if (same) { + *md = sp_digests[i].md; + return true; + } + } + return false; +} + +/* The algorithm argument of crypto.sign/verify: a digest name, or + * null/undefined for the key type's default (SHA-256). */ +static bool sp_algorithm(const ScrDyn *alg, mbedtls_md_type_t *md) { + if (alg->kind == SCR_DYN_UNDEF || alg->kind == SCR_DYN_NULL) { + *md = MBEDTLS_MD_SHA256; + return true; + } + if (alg->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("algorithm", "of type string", alg); + return false; + } + if (!sp_digest_of(alg->v.str, md)) { + char message[160]; + int n = snprintf(message, sizeof message, "Invalid digest: %.*s", (int)alg->v.str->len, + alg->v.str->data); + scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)n, "ERR_CRYPTO_INVALID_DIGEST"); + return false; + } + return true; +} + +typedef struct { + SpKey *key; /* +1 */ + int padding; /* RSA_PKCS1_PADDING (1) or RSA_PKCS1_PSS_PADDING (6) */ + int salt; /* -1 digest length, -2 maximum (sign) or auto (verify) */ + bool p1363; /* dsaEncoding: "ieee-p1363" */ +} SpSigKey; + +/* The key argument of a signing operation: a KeyObject, key material, or + * { key, dsaEncoding, padding, saltLength, ...createKey options }. */ +static bool sp_sig_key_plain(const ScrDyn *value, bool sign, SpSigKey *out); + +static bool sp_sig_key(const ScrDyn *value, bool sign, SpSigKey *out) { + out->key = NULL; + ScrDyn *plain = sp_plain(value); + if (!plain) return false; + bool ok = sp_sig_key_plain(plain, sign, out); + scr_dyn_release(plain); + return ok; +} + +static bool sp_sig_key_plain(const ScrDyn *value, bool sign, SpSigKey *out) { + out->key = NULL; + out->padding = 1; + out->salt = sign ? -2 : -2; + out->p1363 = false; + const ScrDyn *options = NULL; + if (sp_is_object(value) && !sp_key_of(value) && !scr_buffer_storage_is(value)) { + options = value; + const ScrDyn *dsa = sp_opt(value, "dsaEncoding"); + if (sp_is_str(dsa, "ieee-p1363")) out->p1363 = true; + else if (!sp_absent(dsa) && !sp_is_str(dsa, "der")) { + sp_property_invalid("options.dsaEncoding", dsa); + return false; + } + const ScrDyn *padding = sp_opt(value, "padding"); + if (!sp_absent(padding)) { + if (padding->kind != SCR_DYN_NUM || (padding->v.num != 1 && padding->v.num != 6)) { + sp_not_supported("signatures with this padding"); + return false; + } + out->padding = (int)padding->v.num; + } + const ScrDyn *salt = sp_opt(value, "saltLength"); + if (!sp_absent(salt)) { + if (salt->kind != SCR_DYN_NUM || salt->v.num != (double)(int)salt->v.num || salt->v.num < -2) { + sp_not_supported("signatures with this saltLength"); + return false; + } + out->salt = (int)salt->v.num; + } + } + SpKey *direct = sp_key_of(options ? sp_opt(options, "key") : value); + if (direct) { + if (sign && !direct->private_key) { + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE", + "Invalid key object type public, expected private."); + return false; + } + sp_key_retain(direct); + out->key = direct; + return true; + } + ScrDyn *created = sp_create_key((ScrDyn *)value, sign); + if (!created) return false; + out->key = sp_key_retain(sp_key_of(created)); + scr_dyn_release(created); + return true; +} + +/* DER ECDSA-Sig-Value ⇄ IEEE P1363 r || s. */ +static bool sp_der_to_p1363(const unsigned char *der, size_t len, size_t size, unsigned char *out) { + unsigned char *p = (unsigned char *)der; + const unsigned char *end = der + len; + size_t seq; + mbedtls_mpi r, s; + mbedtls_mpi_init(&r); + mbedtls_mpi_init(&s); + bool ok = mbedtls_asn1_get_tag(&p, end, &seq, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) == 0 && + mbedtls_asn1_get_mpi(&p, end, &r) == 0 && mbedtls_asn1_get_mpi(&p, end, &s) == 0 && + mbedtls_mpi_write_binary(&r, out, size) == 0 && + mbedtls_mpi_write_binary(&s, out + size, size) == 0; + mbedtls_mpi_free(&r); + mbedtls_mpi_free(&s); + return ok; +} + +static int sp_p1363_to_der(const unsigned char *sig, size_t len, unsigned char *buf, size_t size) { + mbedtls_mpi r, s; + mbedtls_mpi_init(&r); + mbedtls_mpi_init(&s); + int out = -1; + if (len % 2 == 0 && mbedtls_mpi_read_binary(&r, sig, len / 2) == 0 && + mbedtls_mpi_read_binary(&s, sig + len / 2, len / 2) == 0) { + unsigned char *c = buf + size; + size_t n = 0; + int w; + if ((w = mbedtls_asn1_write_mpi(&c, buf, &s)) >= 0) { + n += (size_t)w; + if ((w = mbedtls_asn1_write_mpi(&c, buf, &r)) >= 0) { + n += (size_t)w; + if ((w = mbedtls_asn1_write_len(&c, buf, n)) >= 0) { + n += (size_t)w; + if ((w = mbedtls_asn1_write_tag(&c, buf, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE)) >= 0) + out = (int)(n + (size_t)w); + } + } + } + } + mbedtls_mpi_free(&r); + mbedtls_mpi_free(&s); + return out; +} + +static size_t sp_ec_size(SpKey *k) { return (mbedtls_pk_ec(k->pk)->MBEDTLS_PRIVATE(grp).nbits + 7) / 8; } + +/* Applies the RSA padding of a signature operation. */ +static bool sp_rsa_padding(SpSigKey *key, mbedtls_md_type_t md) { + if (!sp_is_rsa(key->key)) return true; + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(key->key->pk); + if (key->padding == 6) return mbedtls_rsa_set_padding(rsa, MBEDTLS_RSA_PKCS_V21, md) == 0; + return mbedtls_rsa_set_padding(rsa, MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_NONE) == 0; +} + +/* Signs a digest: +1 Buffer or NULL with the error pending. */ +static ScrDyn *sp_sign_hash(SpSigKey *key, mbedtls_md_type_t md, const unsigned char *hash, size_t hash_len) { + unsigned char sig[MBEDTLS_PK_SIGNATURE_MAX_SIZE]; + size_t sig_len = 0; + int r; + if (!sp_rsa_padding(key, md)) { + sp_unsupported_input(); + return NULL; + } + if (sp_is_rsa(key->key) && key->padding == 6) { + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(key->key->pk); + int salt = key->salt; + size_t bytes = mbedtls_rsa_get_len(rsa); + size_t bits = mbedtls_pk_get_bitlen(&key->key->pk); + size_t max = bytes - hash_len - 2 - ((bits - 1) % 8 == 0 ? 1 : 0); + if (salt == -1) salt = (int)hash_len; + else if (salt == -2) salt = (int)max; + if (salt < 0 || (size_t)salt > max) { + sp_throw(SCR_ERR_ERROR, "ERR_OSSL_RSA_DATA_TOO_LARGE_FOR_KEY_SIZE", + "error:1C8000A5:Provider routines::data too large for key size"); + return NULL; + } + r = mbedtls_rsa_rsassa_pss_sign_ext(rsa, sp_random, NULL, md, (unsigned)hash_len, hash, salt, sig); + sig_len = bytes; + } else { + r = mbedtls_pk_sign(&key->key->pk, md, hash, hash_len, sig, sizeof sig, &sig_len, sp_random, NULL); + } + if (r != 0) { + sp_unsupported_input(); + return NULL; + } + if (sp_is_ec(key->key) && key->p1363) { + size_t size = sp_ec_size(key->key); + unsigned char raw[2 * 66]; + if (!sp_der_to_p1363(sig, sig_len, size, raw)) { + sp_unsupported_input(); + return NULL; + } + return sp_dyn_buffer(raw, 2 * size); + } + return sp_dyn_buffer(sig, sig_len); +} + +/* Verifies a digest against a signature; false for a signature that does + * not verify or does not parse. */ +static bool sp_verify_hash(SpSigKey *key, mbedtls_md_type_t md, const unsigned char *hash, size_t hash_len, + const unsigned char *sig, size_t sig_len) { + if (!sp_rsa_padding(key, md)) return false; + if (sp_is_rsa(key->key) && key->padding == 6) { + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(key->key->pk); + if (sig_len != mbedtls_rsa_get_len(rsa)) return false; + int salt = key->salt; + if (salt == -1) salt = (int)hash_len; + else if (salt == -2) salt = MBEDTLS_RSA_SALT_LEN_ANY; + return mbedtls_rsa_rsassa_pss_verify_ext(rsa, md, (unsigned)hash_len, hash, md, salt, sig) == 0; + } + if (sp_is_ec(key->key) && key->p1363) { + unsigned char der[2 * 66 + 16]; + if (sig_len != 2 * sp_ec_size(key->key)) return false; + int n = sp_p1363_to_der(sig, sig_len, der, sizeof der); + if (n < 0) return false; + return mbedtls_pk_verify(&key->key->pk, md, hash, hash_len, der + sizeof der - n, (size_t)n) == 0; + } + return mbedtls_pk_verify(&key->key->pk, md, hash, hash_len, sig, sig_len) == 0; +} + +static const char sp_data_types[] = "an instance of Buffer, TypedArray, or DataView"; + +/* Ed25519 signs the message itself: an algorithm other than null or + * undefined is OpenSSL's invalid digest (false, error pending). */ +static bool sp_ed_digest_given(const ScrDyn *algorithm) { + if (algorithm->kind == SCR_DYN_UNDEF || algorithm->kind == SCR_DYN_NULL) return true; + sp_throw(SCR_ERR_ERROR, "ERR_OSSL_INVALID_DIGEST", "error:1C80007A:Provider routines::invalid digest"); + return false; +} + +/* crypto.sign(algorithm, data, key): the signature Buffer. */ +ScrDyn *scr_crypto_sign_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key) { + mbedtls_md_type_t md; + if (!sp_algorithm(algorithm, &md)) return NULL; + if (data->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("data", sp_data_types, data); + return NULL; + } + SpSigKey k; + if (!sp_sig_key(key, true, &k)) return NULL; + ScrBytes *bytes = scr_bytes_buffer_source(data); + if (sp_is_ed(k.key)) { + ScrDyn *result = NULL; + const ScrPkiEd25519 *ed = sp_ed_digest_given(algorithm) ? sp_ed() : NULL; + if (ed) { + uint8_t sig[64]; + ed->sign(sig, k.key->ed_seed, k.key->ed_pub, bytes->data, (size_t)scr_bytes_byte_len(bytes)); + result = sp_dyn_buffer(sig, 64); + } + scr_bytes_release(bytes); + sp_key_release(k.key); + return result; + } + unsigned char hash[MBEDTLS_MD_MAX_SIZE]; + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(md); + mbedtls_md(info, bytes->data, (size_t)scr_bytes_byte_len(bytes), hash); + scr_bytes_release(bytes); + ScrDyn *result = sp_sign_hash(&k, md, hash, mbedtls_md_get_size(info)); + sp_key_release(k.key); + return result; +} + +/* crypto.verify(algorithm, data, key, signature): a boolean. */ +ScrDyn *scr_crypto_verify_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key, ScrDyn *signature) { + mbedtls_md_type_t md; + if (!sp_algorithm(algorithm, &md)) return NULL; + if (data->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("data", sp_data_types, data); + return NULL; + } + SpSigKey k; + if (!sp_sig_key(key, false, &k)) return NULL; + if (signature->kind != SCR_DYN_BYTES) { + sp_key_release(k.key); + scr_dyn_arg_type_fail("signature", sp_data_types, signature); + return NULL; + } + ScrBytes *bytes = scr_bytes_buffer_source(data); + ScrBytes *sig = scr_bytes_buffer_source(signature); + if (sp_is_ed(k.key)) { + ScrDyn *result = NULL; + const ScrPkiEd25519 *ed = sp_ed_digest_given(algorithm) ? sp_ed() : NULL; + if (ed) + result = scr_dyn_new_bool(sp_ed_verify(ed, k.key->ed_pub, bytes->data, + (size_t)scr_bytes_byte_len(bytes), sig->data, + (size_t)scr_bytes_byte_len(sig))); + scr_bytes_release(bytes); + scr_bytes_release(sig); + sp_key_release(k.key); + return result; + } + unsigned char hash[MBEDTLS_MD_MAX_SIZE]; + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(md); + mbedtls_md(info, bytes->data, (size_t)scr_bytes_byte_len(bytes), hash); + bool ok = sp_verify_hash(&k, md, hash, mbedtls_md_get_size(info), sig->data, (size_t)scr_bytes_byte_len(sig)); + scr_bytes_release(bytes); + scr_bytes_release(sig); + sp_key_release(k.key); + return scr_dyn_new_bool(ok); +} + +/* ── Sign and Verify objects ──────────────────────────────────────────── */ + +typedef struct SpSigner { + size_t rc; + mbedtls_md_context_t md; + mbedtls_md_type_t type; + bool done; /* sign()/verify() ran: Node's "Not initialised" afterwards */ +} SpSigner; + +static void *sp_signer_retain(void *h) { + ((SpSigner *)h)->rc++; + return h; +} + +static void sp_signer_release(void *h) { + SpSigner *s = h; + if (--s->rc) return; + mbedtls_md_free(&s->md); + free(s); +} + +static void sp_not_initialised(void) { + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INVALID_STATE", "Not initialised"); +} + +/* update(data[, inputEncoding]): strings encode with the encoding (UTF-8 + * by default). */ +static bool sp_signer_update(SpSigner *s, ScrDyn *const *args, size_t argc) { + if (s->done) { + sp_not_initialised(); + return false; + } + const ScrDyn *data = argc ? args[0] : scr_dyn_undefined(); + if (data->kind != SCR_DYN_STR && data->kind != SCR_DYN_BYTES) { + scr_dyn_arg_type_fail("data", "of type string or an instance of Buffer, TypedArray, or DataView", data); + return false; + } + ScrBytes *bytes = sp_source_bytes(data, argc > 1 ? args[1] : NULL); + if (!bytes) return false; + mbedtls_md_update(&s->md, bytes->data, (size_t)scr_bytes_byte_len(bytes)); + scr_bytes_release(bytes); + return true; +} + +/* An output or signature encoding argument: a string encoding, or the + * Buffer form for undefined, null and "buffer". */ +static const ScrStr *sp_encoding_arg(const ScrDyn *v) { + if (v == NULL || v->kind != SCR_DYN_STR || sp_is_str(v, "buffer")) return NULL; + return v->v.str; +} + +static ScrDyn *sp_signer_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what, bool verifier) { + SpSigner *s = h; + if (strcmp(method, "update") == 0) return sp_signer_update(s, args, argc) ? scr_dyn_retain(self) : NULL; + if (!verifier && strcmp(method, "sign") == 0) { + if (s->done) { + sp_not_initialised(); + return NULL; + } + SpSigKey k; + if (!sp_sig_key(argc ? args[0] : scr_dyn_undefined(), true, &k)) return NULL; + if (sp_is_ed(k.key)) { + sp_key_release(k.key); + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNSUPPORTED_OPERATION", "Unsupported crypto operation"); + return NULL; + } + unsigned char hash[MBEDTLS_MD_MAX_SIZE]; + mbedtls_md_finish(&s->md, hash); + s->done = true; + ScrDyn *sig = sp_sign_hash(&k, s->type, hash, mbedtls_md_get_size(mbedtls_md_info_from_type(s->type))); + sp_key_release(k.key); + const ScrStr *encoding = sp_encoding_arg(argc > 1 ? args[1] : NULL); + if (!sig || !encoding) return sig; + ScrBytes *bytes = scr_bytes_buffer_source(sig); + ScrStr *text = scr_bytes_to_str_checked(bytes, encoding); + scr_bytes_release(bytes); + scr_dyn_release(sig); + return text ? sp_dyn_str(text) : NULL; + } + if (verifier && strcmp(method, "verify") == 0) { + if (s->done) { + sp_not_initialised(); + return NULL; + } + SpSigKey k; + if (!sp_sig_key(argc ? args[0] : scr_dyn_undefined(), false, &k)) return NULL; + if (sp_is_ed(k.key)) { + sp_key_release(k.key); + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNSUPPORTED_OPERATION", "Unsupported crypto operation"); + return NULL; + } + const ScrDyn *signature = argc > 1 ? args[1] : scr_dyn_undefined(); + if (signature->kind != SCR_DYN_STR && signature->kind != SCR_DYN_BYTES) { + sp_key_release(k.key); + scr_dyn_arg_type_fail("signature", "of type string or an instance of Buffer, TypedArray, or DataView", + signature); + return NULL; + } + const ScrStr *encoding = sp_encoding_arg(argc > 2 ? args[2] : NULL); + ScrDyn *encoding_value = encoding ? scr_dyn_new_str((ScrStr *)encoding) : NULL; + ScrBytes *sig = sp_source_bytes(signature, encoding_value); + if (encoding_value) scr_dyn_release(encoding_value); + if (!sig) { + sp_key_release(k.key); + return NULL; + } + unsigned char hash[MBEDTLS_MD_MAX_SIZE]; + mbedtls_md_finish(&s->md, hash); + s->done = true; + bool ok = sp_verify_hash(&k, s->type, hash, mbedtls_md_get_size(mbedtls_md_info_from_type(s->type)), + sig->data, (size_t)scr_bytes_byte_len(sig)); + scr_bytes_release(sig); + sp_key_release(k.key); + return scr_dyn_new_bool(ok); + } + char message[160]; + int n = snprintf(message, sizeof message, "%s is not a function", what); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + return NULL; +} + +static ScrDyn *sp_sign_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + return sp_signer_invoke(h, self, method, args, argc, what, false); +} + +static ScrDyn *sp_verify_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + return sp_signer_invoke(h, self, method, args, argc, what, true); +} + +/* Sign and Verify have no modeled properties. */ +static ScrDyn *sp_signer_get(void *h, const char *name, size_t len) { + (void)h; + (void)name; + (void)len; + return NULL; +} + +static bool sp_signer_set(void *h, const char *name, size_t len, const ScrDyn *value) { + (void)h; + (void)name; + (void)len; + (void)value; + return false; +} + +static ScrDyn *sp_signer_inspect_view(void *h) { + (void)h; + return scr_dyn_new_obj(); +} + +static const ScrDynHandleOps sp_sign_ops = { + .cls = "Sign", + .retain = sp_signer_retain, + .release = sp_signer_release, + .invoke = sp_sign_invoke, + .get = sp_signer_get, + .set = sp_signer_set, + .inspect_view = sp_signer_inspect_view, +}; + +static const ScrDynHandleOps sp_verify_ops = { + .cls = "Verify", + .retain = sp_signer_retain, + .release = sp_signer_release, + .invoke = sp_verify_invoke, + .get = sp_signer_get, + .set = sp_signer_set, + .inspect_view = sp_signer_inspect_view, +}; + +/* createSign(algorithm) / createVerify(algorithm). */ +static ScrDyn *sp_signer_new(ScrDyn *algorithm, bool verifier) { + if (algorithm->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("algorithm", "of type string", algorithm); + return NULL; + } + mbedtls_md_type_t md; + if (!sp_digest_of(algorithm->v.str, &md)) { + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_DIGEST", "Invalid digest"); + return NULL; + } + static SCR_TL bool installed; + if (!installed) { + installed = true; + scr_dyn_handle_install(SCR_DYNH_SIGN, &sp_sign_ops); + scr_dyn_handle_install(SCR_DYNH_VERIFY, &sp_verify_ops); + } + SpSigner *s = calloc(1, sizeof *s); + if (!s) scr_trap("scriptc: out of memory\n"); + s->rc = 1; + s->type = md; + mbedtls_md_init(&s->md); + mbedtls_md_setup(&s->md, mbedtls_md_info_from_type(md), 0); + mbedtls_md_starts(&s->md); + ScrDyn *box = scr_dyn_new_handle(s, verifier ? SCR_DYNH_VERIFY : SCR_DYNH_SIGN); + sp_signer_release(s); + return box; +} + +ScrDyn *scr_crypto_create_sign(ScrDyn *algorithm) { return sp_signer_new(algorithm, false); } + +ScrDyn *scr_crypto_create_verify(ScrDyn *algorithm) { return sp_signer_new(algorithm, true); } + +/* ── generateKeyPair ──────────────────────────────────────────────────── */ + +static ScrDyn *sp_cap(ScrClosure *c, size_t i) { return scr_box_get_ref(c->caps[i]); } + +static void sp_key_pair_deliver(ScrClosure *self) { + ScrDyn *callback = sp_cap(self, 0); + ScrDyn *pair = sp_cap(self, 1); + ScrDyn *args[3] = {scr_dyn_new_null(), scr_dyn_retain(scr_dyn_obj_get(pair, "publicKey", 9)), + scr_dyn_retain(scr_dyn_obj_get(pair, "privateKey", 10))}; + ScrDyn *result = scr_dyn_call(callback, args, 3, "callback"); + if (result) scr_dyn_release(result); + for (size_t i = 0; i < 3; i++) scr_dyn_release(args[i]); + scr_dyn_release(pair); + scr_dyn_release(callback); +} + +/* generateKeyPair(type, options, callback): argument errors throw, and the + * callback receives (null, publicKey, privateKey) in a later macrotask. */ +void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *callback) { + ScrDyn *pair = scr_crypto_generate_key_pair_sync(type, options); + if (!pair) return; + ScrClosure *c = scr_closure_new((void *)sp_key_pair_deliver, 2); + c->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[0], scr_dyn_retain(callback)); + c->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[1], pair); + scr_loop_defer(c); +} + +/* ── the core for the WebCrypto layer (scr_pki.h) ─────────────────────── */ + +ScrPkiKey *scr_pki_key_retain(ScrPkiKey *key) { return sp_key_retain(key); } + +void scr_pki_key_release(ScrPkiKey *key) { sp_key_release(key); } + +bool scr_pki_key_is_private(const ScrPkiKey *key) { return key->private_key; } + +bool scr_pki_key_is_rsa(const ScrPkiKey *key) { return sp_is_rsa(key); } + +bool scr_pki_key_is_ed25519(const ScrPkiKey *key) { return sp_is_ed(key); } + +const char *scr_pki_key_curve(const ScrPkiKey *key) { + const SpCurve *curve = sp_curve_of(key); + return curve ? curve->jwk : NULL; +} + +size_t scr_pki_key_bits(const ScrPkiKey *key) { return mbedtls_pk_get_bitlen(&key->pk); } + +ScrBytes *scr_pki_key_exponent(const ScrPkiKey *key) { + mbedtls_mpi e; + mbedtls_mpi_init(&e); + mbedtls_rsa_export(mbedtls_pk_rsa(key->pk), NULL, NULL, NULL, NULL, &e); + size_t n = mbedtls_mpi_size(&e); + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)n); + if (n) mbedtls_mpi_write_binary(&e, out->data, n); + mbedtls_mpi_free(&e); + return out; +} + +ScrPkiKey *scr_pki_key_parse_der(const uint8_t *der, size_t len, bool private_key) { + if ((private_key && len == 48 && memcmp(der, sp_ed_pkcs8_prefix, 16) == 0) || + (!private_key && len == 44 && memcmp(der, sp_ed_spki_prefix, 12) == 0)) + { + bool failed; + return sp_ed_from_der(der, len, private_key, &failed); + } + SpKey *k = sp_key_alloc(private_key); + int r = private_key ? mbedtls_pk_parse_key(&k->pk, der, len, NULL, 0, sp_random, NULL) + : mbedtls_pk_parse_public_key(&k->pk, der, len); + if (r != 0 || !sp_key_supported(k)) { + sp_key_release(k); + return NULL; + } + return k; +} + +ScrPkiKey *scr_pki_key_from_jwk(const ScrDyn *jwk, bool private_key) { + SpKey *k = sp_key_from_jwk(jwk, private_key); + if (!k) scr_exc_clear(); + return k; +} + +ScrPkiKey *scr_pki_key_from_point(const char *curve_name, const uint8_t *point, size_t len) { + const SpCurve *curve = NULL; + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (strcmp(sp_curves[i].jwk, curve_name) == 0) curve = &sp_curves[i]; + if (!curve) return NULL; + SpKey *k = sp_key_alloc(false); + bool ok = false; + if (mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) == 0) { + mbedtls_ecp_keypair *ec = mbedtls_pk_ec(k->pk); + ok = mbedtls_ecp_group_load(&ec->MBEDTLS_PRIVATE(grp), curve->id) == 0 && + mbedtls_ecp_point_read_binary(&ec->MBEDTLS_PRIVATE(grp), &ec->MBEDTLS_PRIVATE(Q), point, len) == 0 && + mbedtls_ecp_check_pubkey(&ec->MBEDTLS_PRIVATE(grp), &ec->MBEDTLS_PRIVATE(Q)) == 0; + } + if (!ok) { + sp_key_release(k); + return NULL; + } + return k; +} + +ScrPkiKey *scr_pki_key_public_of(ScrPkiKey *key) { return sp_public_of(key); } + +ScrPkiKey *scr_pki_generate_rsa(unsigned bits, uint64_t exponent) { + SpKey *k = sp_key_alloc(true); + if (exponent > 2147483647 || mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0 || + mbedtls_rsa_gen_key(mbedtls_pk_rsa(k->pk), sp_random, NULL, bits, (int)exponent) != 0) { + sp_key_release(k); + return NULL; + } + return k; +} + +ScrPkiKey *scr_pki_generate_ec(const char *curve_name) { + const SpCurve *curve = NULL; + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (strcmp(sp_curves[i].jwk, curve_name) == 0) curve = &sp_curves[i]; + SpKey *k = sp_key_alloc(true); + if (!curve || mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) != 0 || + mbedtls_ecp_gen_key(curve->id, mbedtls_pk_ec(k->pk), sp_random, NULL) != 0) { + sp_key_release(k); + return NULL; + } + return k; +} + +ScrPkiKey *scr_pki_generate_ed25519(void) { + const ScrPkiEd25519 *ed = sp_ed(); + if (!ed) return NULL; + SpKey *k = sp_key_alloc(true); + k->ed25519 = true; + sp_random(NULL, k->ed_seed, 32); + ed->public_key(k->ed_pub, k->ed_seed); + return k; +} + +ScrPkiKey *scr_pki_key_from_ed25519_raw(const uint8_t *raw, size_t len) { + if (len != 32) return NULL; + SpKey *k = sp_key_alloc(false); + k->ed25519 = true; + memcpy(k->ed_pub, raw, 32); + return k; +} + +ScrBytes *scr_pki_export_ed25519_raw(ScrPkiKey *key) { + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, 32); + memcpy(out->data, key->ed_pub, 32); + return out; +} + +ScrBytes *scr_pki_export_der(ScrPkiKey *key) { + unsigned char *der = malloc(SP_DER_MAX); + if (!der) scr_trap("scriptc: out of memory\n"); + int n = key->private_key ? sp_write_pkcs8_der(key, der, SP_DER_MAX) : sp_write_spki_der(key, der, SP_DER_MAX); + ScrBytes *out = NULL; + if (n > 0) { + out = scr_bytes_new(SCR_BYTES_U8, (double)n); + memcpy(out->data, der + SP_DER_MAX - n, (size_t)n); + } + mbedtls_platform_zeroize(der, SP_DER_MAX); + free(der); + return out; +} + +ScrBytes *scr_pki_export_point(ScrPkiKey *key) { + const mbedtls_ecp_keypair *ec = mbedtls_pk_ec(key->pk); + unsigned char point[160]; + size_t n; + if (mbedtls_ecp_point_write_binary(&ec->MBEDTLS_PRIVATE(grp), &ec->MBEDTLS_PRIVATE(Q), + MBEDTLS_ECP_PF_UNCOMPRESSED, &n, point, sizeof point) != 0) + return NULL; + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)n); + memcpy(out->data, point, n); + return out; +} + +ScrDyn *scr_pki_export_jwk(ScrPkiKey *key) { return sp_export_jwk(key); } + +static bool sp_webcrypto_md(const char *hash, mbedtls_md_type_t *md) { + if (strcmp(hash, "SHA-1") == 0) *md = MBEDTLS_MD_SHA1; + else if (strcmp(hash, "SHA-256") == 0) *md = MBEDTLS_MD_SHA256; + else if (strcmp(hash, "SHA-384") == 0) *md = MBEDTLS_MD_SHA384; + else if (strcmp(hash, "SHA-512") == 0) *md = MBEDTLS_MD_SHA512; + else return false; + return true; +} + +ScrBytes *scr_pki_sign(ScrPkiKey *key, const char *hash, bool pss, int salt, const uint8_t *data, + size_t len) { + if (sp_is_ed(key)) { + const ScrPkiEd25519 *ed = sp_ed(); + if (!ed) return NULL; + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, 64); + ed->sign(out->data, key->ed_seed, key->ed_pub, data, len); + return out; + } + mbedtls_md_type_t md; + if (!sp_webcrypto_md(hash, &md)) return NULL; + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(md); + unsigned char digest[MBEDTLS_MD_MAX_SIZE]; + mbedtls_md(info, data, len, digest); + SpSigKey k = {.key = key, .padding = pss ? 6 : 1, .salt = salt, .p1363 = true}; + ScrDyn *sig = sp_sign_hash(&k, md, digest, mbedtls_md_get_size(info)); + if (!sig) { + scr_exc_clear(); + return NULL; + } + ScrBytes *out = scr_bytes_buffer_source(sig); + scr_dyn_release(sig); + return out; +} + +bool scr_pki_verify(ScrPkiKey *key, const char *hash, bool pss, int salt, const uint8_t *data, + size_t len, const uint8_t *signature, size_t signature_len) { + if (sp_is_ed(key)) { + const ScrPkiEd25519 *ed = sp_ed(); + return ed && sp_ed_verify(ed, key->ed_pub, data, len, signature, signature_len); + } + mbedtls_md_type_t md; + if (!sp_webcrypto_md(hash, &md)) return false; + const mbedtls_md_info_t *info = mbedtls_md_info_from_type(md); + unsigned char digest[MBEDTLS_MD_MAX_SIZE]; + mbedtls_md(info, data, len, digest); + SpSigKey k = {.key = key, .padding = pss ? 6 : 1, .salt = salt, .p1363 = true}; + return sp_verify_hash(&k, md, digest, mbedtls_md_get_size(info), signature, signature_len); +} diff --git a/packages/runtime/src/scr_pki.h b/packages/runtime/src/scr_pki.h new file mode 100644 index 0000000000..e299806f71 --- /dev/null +++ b/packages/runtime/src/scr_pki.h @@ -0,0 +1,75 @@ +/* The key and signature core of scr_pki.c, for the WebCrypto layer + * (scr_webcrypto_pki.c) and the Ed25519 unit (scr_ed25519.c). Keys are + * reference-counted RSA keys, EC keys on P-256, P-384, P-521 or secp256k1, + * or Ed25519 keys. Functions answering NULL or false set no error unless + * they say so; byte results are +1. */ +#ifndef SCR_PKI_H +#define SCR_PKI_H + +#include +#include +#include + +typedef struct SpKey ScrPkiKey; + +/* The Ed25519 arithmetic (scr_ed25519.c), installed at startup in + * programs that name Ed25519; NULL otherwise. Keys are the 32-byte seed + * and the 32-byte encoded public point. */ +typedef struct { + void (*public_key)(uint8_t pub[32], const uint8_t seed[32]); + void (*sign)(uint8_t sig[64], const uint8_t seed[32], const uint8_t pub[32], const uint8_t *m, size_t n); + bool (*verify)(const uint8_t pub[32], const uint8_t *m, size_t n, const uint8_t sig[64]); +} ScrPkiEd25519; +extern const ScrPkiEd25519 *scr_pki_ed25519; + +/* Whether the Ed25519 arithmetic is linked; false with a coded SC2020 + * error pending otherwise. */ +bool scr_pki_ed25519_ready(void); + +ScrPkiKey *scr_pki_key_retain(ScrPkiKey *key); +void scr_pki_key_release(ScrPkiKey *key); +bool scr_pki_key_is_private(const ScrPkiKey *key); +bool scr_pki_key_is_rsa(const ScrPkiKey *key); +bool scr_pki_key_is_ed25519(const ScrPkiKey *key); +/* The EC key's JWK curve name ("P-256", ...), or NULL for RSA keys. */ +const char *scr_pki_key_curve(const ScrPkiKey *key); +/* The RSA modulus length in bits. */ +size_t scr_pki_key_bits(const ScrPkiKey *key); +/* The RSA public exponent's big-endian bytes, without leading zeros. */ +ScrBytes *scr_pki_key_exponent(const ScrPkiKey *key); + +/* A key from SubjectPublicKeyInfo (public) or PKCS#8 (private) DER. */ +ScrPkiKey *scr_pki_key_parse_der(const uint8_t *der, size_t len, bool private_key); +/* A key from a JWK object (borrowed; a plain object). */ +ScrPkiKey *scr_pki_key_from_jwk(const ScrDyn *jwk, bool private_key); +/* A public EC key from an uncompressed or compressed point. */ +ScrPkiKey *scr_pki_key_from_point(const char *curve, const uint8_t *point, size_t len); +ScrPkiKey *scr_pki_key_public_of(ScrPkiKey *key); +ScrPkiKey *scr_pki_generate_rsa(unsigned bits, uint64_t exponent); +ScrPkiKey *scr_pki_generate_ec(const char *curve); +/* NULL with a coded SC2020 error pending when the program does not link + * the Ed25519 unit. */ +ScrPkiKey *scr_pki_generate_ed25519(void); +/* A public Ed25519 key from its 32-byte encoding. */ +ScrPkiKey *scr_pki_key_from_ed25519_raw(const uint8_t *raw, size_t len); +/* The Ed25519 public key's 32-byte encoding. */ +ScrBytes *scr_pki_export_ed25519_raw(ScrPkiKey *key); + +/* SubjectPublicKeyInfo (public keys) or PKCS#8 (private keys) DER. */ +ScrBytes *scr_pki_export_der(ScrPkiKey *key); +/* The EC public key's uncompressed point. */ +ScrBytes *scr_pki_export_point(ScrPkiKey *key); +/* The JWK members in Node's order (kty, n, e, ... or kty, x, y, crv, d), + * +1. */ +ScrDyn *scr_pki_export_jwk(ScrPkiKey *key); + +/* Signs `data` with the digest named by its WebCrypto name ("SHA-256"): + * RSA PKCS#1 v1.5 (pss false), RSA-PSS with `salt` bytes, or ECDSA as + * IEEE P1363 r || s. Ed25519 keys sign the message itself (hash + * ignored). */ +ScrBytes *scr_pki_sign(ScrPkiKey *key, const char *hash, bool pss, int salt, const uint8_t *data, + size_t len); +bool scr_pki_verify(ScrPkiKey *key, const char *hash, bool pss, int salt, const uint8_t *data, + size_t len, const uint8_t *signature, size_t signature_len); + +#endif diff --git a/packages/runtime/src/scr_webcrypto.c b/packages/runtime/src/scr_webcrypto.c index 546b241518..a11b22fe49 100644 --- a/packages/runtime/src/scr_webcrypto.c +++ b/packages/runtime/src/scr_webcrypto.c @@ -18,6 +18,7 @@ * SC2020 Error instead of silently diverging. The compiler links the unit * only into programs that call these methods (the webcrypto feature). */ #include "scr_runtime.h" +#include "scr_webcrypto.h" #include #include @@ -43,13 +44,12 @@ static const char *const scr_wc_sign_names[] = { "ECDSA", "Ed25519", "Ed448", "HMAC", "KMAC128", "KMAC256", "ML-DSA-44", "ML-DSA-65", "ML-DSA-87", "RSA-PSS", "RSASSA-PKCS1-v1_5", }; -static const char *const scr_wc_digest_names[] = { +const char *const scr_wc_digest_names[] = { "SHA-1", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", "SHA3-512", "cSHAKE128", "cSHAKE256", "KT128", "KT256", "TurboSHAKE128", "TurboSHAKE256", }; -#define SCR_WC_COUNT(names) (sizeof(names) / sizeof((names)[0])) /* The implemented hashes: WebCrypto name and node:crypto digest name. */ static const struct { @@ -75,7 +75,7 @@ static bool scr_wc_ascii_ieq(const ScrStr *s, const char *name) { } /* The canonical registered spelling of `name`, or NULL. */ -static const char *scr_wc_lookup(const char *const *names, size_t count, const ScrStr *name) { +const char *scr_wc_lookup(const char *const *names, size_t count, const ScrStr *name) { for (size_t i = 0; i < count; i++) if (scr_wc_ascii_ieq(name, names[i])) return names[i]; return NULL; @@ -89,11 +89,9 @@ static const char *scr_wc_native_hash(const char *name) { /* ── errors ────────────────────────────────────────────────────────── */ -static const char *const scr_wc_ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"}; +const char *const scr_wc_ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"}; -static void scr_wc_type_error(const char *code, const char *fmt, ...) - __attribute__((format(printf, 2, 3))); -static void scr_wc_type_error(const char *code, const char *fmt, ...) { +void scr_wc_type_error(const char *code, const char *fmt, ...) { char message[512]; va_list ap; va_start(ap, fmt); @@ -105,7 +103,7 @@ static void scr_wc_type_error(const char *code, const char *fmt, ...) { } /* An operation or algorithm Node supports that this slice does not. */ -static void scr_wc_unsupported(const char *method, const char *what) { +void scr_wc_unsupported(const char *method, const char *what) { char message[256]; int n = snprintf(message, sizeof message, "crypto.subtle.%s with %s is not supported yet", method, what); @@ -113,7 +111,7 @@ static void scr_wc_unsupported(const char *method, const char *what) { scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020"); } -static void scr_wc_unsupported_algorithm(const char *method, const char *name) { +void scr_wc_unsupported_algorithm(const char *method, const char *name) { char what[96]; snprintf(what, sizeof what, "the %s algorithm", name); scr_wc_unsupported(method, what); @@ -123,7 +121,7 @@ static void scr_wc_unsupported_algorithm(const char *method, const char *name) { * Each answers NULL/false with the TypeError pending. `prefix` is * "Failed to execute '' on 'SubtleCrypto'". */ -static bool scr_wc_is_object(const ScrDyn *v) { +bool scr_wc_is_object(const ScrDyn *v) { switch (v->kind) { case SCR_DYN_UNDEF: case SCR_DYN_NULL: @@ -139,14 +137,14 @@ static bool scr_wc_is_object(const ScrDyn *v) { } /* A typed reference capsule as its plain view; other values retained. */ -static ScrDyn *scr_wc_plain(ScrDyn *v) { +ScrDyn *scr_wc_plain(ScrDyn *v) { if (v->kind == SCR_DYN_TYPED_REF) return scr_dyn_typed_ref_materialize(v); return scr_dyn_retain(v); } /* BufferSource: the bytes of a TypedArray, DataView, Buffer or * ArrayBuffer as a u8 view (+1). */ -static ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn *value) { +ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn *value) { ScrDyn *plain = scr_wc_plain(value); if (!plain) return NULL; ScrBytes *view = NULL; @@ -163,14 +161,14 @@ static ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn * } /* DOMString conversion (+1). */ -static ScrStr *scr_wc_dom_string(const ScrDyn *value) { return scr_dyn_string_coerce_js(value); } +ScrStr *scr_wc_dom_string(const ScrDyn *value) { return scr_dyn_string_coerce_js(value); } static const char *const scr_wc_key_formats[] = { "raw", "raw-public", "raw-seed", "raw-secret", "raw-private", "pkcs8", "spki", "jwk", }; /* KeyFormat: the canonical enum spelling, or NULL. */ -static const char *scr_wc_key_format(const char *prefix, ScrDyn *value) { +const char *scr_wc_key_format(const char *prefix, ScrDyn *value) { ScrStr *s = scr_wc_dom_string(value); if (!s) return NULL; for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_key_formats); i++) { @@ -188,15 +186,14 @@ static const char *scr_wc_key_format(const char *prefix, ScrDyn *value) { } /* Node's canonical usage order; the bit is the index. */ -static const char *const scr_wc_usages[] = { +const char *const scr_wc_usages[] = { "encrypt", "decrypt", "sign", "verify", "deriveKey", "deriveBits", "wrapKey", "unwrapKey", "encapsulateKey", "encapsulateBits", "decapsulateKey", "decapsulateBits", }; -enum { SCR_WC_USAGE_SIGN = 1u << 2, SCR_WC_USAGE_VERIFY = 1u << 3 }; /* sequence over an array: the usage mask (-1 on failure). */ -static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { +long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { ScrDyn *plain = scr_wc_plain(value); if (!plain) return -1; if (plain->kind != SCR_DYN_ARR) { @@ -245,9 +242,9 @@ typedef struct { double length; } ScrWcAlgorithm; -static const char scr_wc_normalize_prefix[] = "Failed to normalize algorithm"; +const char scr_wc_normalize_prefix[] = "Failed to normalize algorithm"; -static void scr_wc_missing_member(const char *dict, const char *member) { +void scr_wc_missing_member(const char *dict, const char *member) { scr_wc_type_error("ERR_MISSING_OPTION", "%s: passed algorithm cannot be converted to '%s' because '%s' is required in " "'%s'.", @@ -255,7 +252,7 @@ static void scr_wc_missing_member(const char *dict, const char *member) { } /* Reads `key` off an algorithm dictionary (+1; undefined when absent). */ -static ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key) { +ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key) { if (dict->kind != SCR_DYN_OBJ) return scr_dyn_retain(scr_dyn_undefined()); ScrDyn *v = scr_dyn_obj_read(dict, key, strlen(key)); return v ? v : scr_dyn_retain(scr_dyn_undefined()); @@ -264,7 +261,7 @@ static ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key) { /* The name an AlgorithmIdentifier carries (+1), with the Algorithm * dictionary's missing-name TypeError. `dict` names the dictionary the * value converts to. */ -static ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict) { +ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict) { if (!scr_wc_is_object(identifier)) return scr_wc_dom_string(identifier); ScrDyn *plain = scr_wc_plain(identifier); if (!plain) return NULL; @@ -279,7 +276,7 @@ static ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict) { /* The digest a HashAlgorithmIdentifier names: canonical name, or NULL * with NotSupportedError / the SC2020 refusal pending. */ -static const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *method) { +const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *method) { ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); if (!name) return NULL; const char *canonical = scr_wc_lookup(scr_wc_digest_names, SCR_WC_COUNT(scr_wc_digest_names), name); @@ -436,10 +433,10 @@ static ScrCryptoKey *scr_crypto_key_of(const ScrDyn *value) { bool scr_webcrypto_is_crypto_key(const ScrDyn *value) { if (value->kind == SCR_DYN_TYPED_REF) return false; - return scr_crypto_key_of(value) != NULL; + return scr_crypto_key_of(value) != NULL || (scr_wc_asymmetric && scr_wc_asymmetric->is_key(value)); } -static ScrDyn *scr_wc_cstr(const char *s) { +ScrDyn *scr_wc_cstr(const char *s) { ScrStr *str = scr_str_new(s, strlen(s)); ScrDyn *out = scr_dyn_new_str(str); scr_str_release(str); @@ -567,7 +564,43 @@ static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const c * Each takes its argument vector as one dyn array (borrowed) and answers * a +1 promise. */ -static ScrPromise *scr_wc_rejected(void) { +const ScrWcAsymmetric *scr_wc_asymmetric = NULL; + +static const char *const scr_wc_asymmetric_names[] = {"ECDSA", "Ed25519", "RSA-PSS", "RSASSA-PKCS1-v1_5"}; + +/* Whether the AlgorithmIdentifier at args[index] names ECDSA, + * RSASSA-PKCS1-v1_5, RSA-PSS or Ed25519, read without side effects or errors: a + * string, or the name member of an object (a statically typed one through + * its plain view). A value that does not read cleanly takes the HMAC path, + * which reports its errors. */ +static const char *scr_wc_asymmetric_name(const ScrDyn *args, size_t index) { + if ((size_t)scr_dyn_arr_len(args) <= index) return NULL; + ScrDyn *identifier = scr_dyn_arr_at(args, (double)index); + ScrDyn *plain = scr_wc_plain(identifier); + if (!plain) scr_exc_clear(); + const char *found = NULL; + if (plain) { + const ScrDyn *name = plain; + if (plain->kind == SCR_DYN_OBJ) name = scr_dyn_obj_get(plain, "name", 4); + if (name && name->kind == SCR_DYN_STR) + found = scr_wc_lookup(scr_wc_asymmetric_names, SCR_WC_COUNT(scr_wc_asymmetric_names), + name->v.str); + scr_dyn_release(plain); + } + scr_dyn_release(identifier); + return found; +} + +/* The asymmetric algorithms run in scr_webcrypto_pki.c, which the compiler + * links when a string literal in the program names one of them. */ +static ScrPromise *scr_wc_asymmetric_missing(const char *method, const char *name) { + char what[160]; + snprintf(what, sizeof what, "the %s algorithm named only by a computed string", name); + scr_wc_unsupported(method, what); + return scr_wc_rejected(); +} + +ScrPromise *scr_wc_rejected(void) { ScrPromise *p = scr_promise_new(); scr_promise_reject_pending(p); return p; @@ -575,7 +608,7 @@ static ScrPromise *scr_wc_rejected(void) { /* The argument count check every method starts with (after the receiver * check the compiler's call shape already guarantees). */ -static bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required) { +bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required) { size_t argc = (size_t)scr_dyn_arr_len(args); if (argc >= required) return true; scr_wc_type_error("ERR_MISSING_ARGS", "%s: %zu arguments required, but only %zu present.", prefix, @@ -583,15 +616,12 @@ static bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t requi return false; } -typedef struct { - ScrDyn *v[5]; -} ScrWcArgs; -static void scr_wc_args_take(ScrWcArgs *a, const ScrDyn *args, size_t n) { +void scr_wc_args_take(ScrWcArgs *a, const ScrDyn *args, size_t n) { for (size_t i = 0; i < n; i++) a->v[i] = scr_dyn_arr_at(args, (double)i); } -static void scr_wc_args_drop(ScrWcArgs *a, size_t n) { +void scr_wc_args_drop(ScrWcArgs *a, size_t n) { for (size_t i = 0; i < n; i++) scr_dyn_release(a->v[i]); } @@ -681,6 +711,11 @@ static ScrDyn *scr_wc_import(const ScrDyn *args) { } ScrPromise *scr_webcrypto_import_key(ScrDyn *args) { + const char *asymmetric = scr_wc_asymmetric_name(args, 2); + if (asymmetric) { + return scr_wc_asymmetric ? scr_wc_asymmetric->import_key(args) + : scr_wc_asymmetric_missing("importKey", asymmetric); + } ScrDyn *key = scr_wc_import(args); if (!key) return scr_wc_rejected(); return scr_promise_settled_ref(key, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); @@ -706,7 +741,8 @@ static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char ma scr_str_release(s); } ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); - if (!key) { + bool asymmetric_key = !key && scr_wc_asymmetric && scr_wc_asymmetric->is_key(a.v[1]); + if (!key && !asymmetric_key) { scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); goto done; } @@ -724,6 +760,10 @@ static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char ma scr_wc_unsupported_algorithm(method, algorithm.name); goto done; } + if (asymmetric_key) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + goto done; + } if (!(key->usages & (verify ? SCR_WC_USAGE_VERIFY : SCR_WC_USAGE_SIGN))) { char message[64]; snprintf(message, sizeof message, "Unable to use this key to %s", method); @@ -744,6 +784,11 @@ static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char ma } ScrPromise *scr_webcrypto_sign(ScrDyn *args) { + const char *asymmetric = scr_wc_asymmetric_name(args, 0); + if (asymmetric) { + return scr_wc_asymmetric ? scr_wc_asymmetric->sign(args) + : scr_wc_asymmetric_missing("sign", asymmetric); + } unsigned char mac[64]; size_t mac_len = 0; if (!scr_wc_sign_verify(args, false, mac, &mac_len, NULL)) return scr_wc_rejected(); @@ -766,6 +811,11 @@ static void scr_wc_settle_verified(ScrClosure *cb, ScrError *error, ScrBytes *va } ScrPromise *scr_webcrypto_verify(ScrDyn *args) { + const char *asymmetric = scr_wc_asymmetric_name(args, 0); + if (asymmetric) { + return scr_wc_asymmetric ? scr_wc_asymmetric->verify(args) + : scr_wc_asymmetric_missing("verify", asymmetric); + } unsigned char mac[64]; size_t mac_len = 0; ScrBytes *signature = NULL; @@ -827,3 +877,53 @@ ScrPromise *scr_webcrypto_digest(ScrDyn *args) { scr_crypto_promise_settle_array_buffer); return p; } + +/* generateKey(algorithm, extractable, keyUsages): ECDSA, + * RSASSA-PKCS1-v1_5 and RSA-PSS key pairs (scr_webcrypto_pki.c); other + * recognized algorithms are refused after Node's argument conversions. */ +ScrPromise *scr_webcrypto_generate_key(ScrDyn *args) { + const char *asymmetric = scr_wc_asymmetric_name(args, 0); + if (asymmetric) { + return scr_wc_asymmetric ? scr_wc_asymmetric->generate_key(args) + : scr_wc_asymmetric_missing("generateKey", asymmetric); + } + static const char prefix[] = "Failed to execute 'generateKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 3)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 3); + bool converted = true; + if (!scr_wc_is_object(a.v[0])) { + ScrStr *str = scr_wc_dom_string(a.v[0]); + if (str) scr_str_release(str); + else converted = false; + } + if (converted && scr_wc_key_usages(prefix, 2, a.v[2]) >= 0) { + ScrStr *name = scr_wc_algorithm_name(a.v[0], "Algorithm"); + if (name) { + const char *known = scr_wc_lookup(scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), name); + scr_str_release(name); + if (!known) scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + else scr_wc_unsupported_algorithm("generateKey", known); + } + } + scr_wc_args_drop(&a, 3); + return scr_wc_rejected(); +} + +/* exportKey(format, key): ECDSA and RSA keys (scr_webcrypto_pki.c); HMAC + * keys are refused after Node's argument conversions. */ +ScrPromise *scr_webcrypto_export_key(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'exportKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 2)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 2); + ScrPromise *result = NULL; + if (scr_wc_key_format(prefix, a.v[0])) { + if (scr_wc_asymmetric && scr_wc_asymmetric->is_key(a.v[1])) result = scr_wc_asymmetric->export_key(args); + else if (scr_crypto_key_of(a.v[1])) scr_wc_unsupported("exportKey", "HMAC keys"); + else + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + } + scr_wc_args_drop(&a, 2); + return result ? result : scr_wc_rejected(); +} diff --git a/packages/runtime/src/scr_webcrypto.h b/packages/runtime/src/scr_webcrypto.h new file mode 100644 index 0000000000..c8d7d83bc6 --- /dev/null +++ b/packages/runtime/src/scr_webcrypto.h @@ -0,0 +1,58 @@ +/* The Web IDL and error helpers of scr_webcrypto.c that the asymmetric + * algorithms (scr_webcrypto_pki.c) share, and the table through which + * scr_webcrypto.c reaches them when that unit is linked. */ +#ifndef SCR_WEBCRYPTO_H +#define SCR_WEBCRYPTO_H + +#include +#include + +#define SCR_WC_COUNT(names) (sizeof(names) / sizeof((names)[0])) + +enum { SCR_WC_USAGE_SIGN = 1u << 2, SCR_WC_USAGE_VERIFY = 1u << 3 }; + +/* Node's canonical usage order; the bit is the index. */ +extern const char *const scr_wc_usages[12]; +extern const char *const scr_wc_ordinals[5]; +extern const char scr_wc_normalize_prefix[]; +extern const char *const scr_wc_digest_names[13]; + +typedef struct { + ScrDyn *v[5]; +} ScrWcArgs; + +void scr_wc_type_error(const char *code, const char *fmt, ...) __attribute__((format(printf, 2, 3))); +void scr_wc_unsupported(const char *method, const char *what); +void scr_wc_unsupported_algorithm(const char *method, const char *name); +bool scr_wc_is_object(const ScrDyn *v); +ScrDyn *scr_wc_plain(ScrDyn *v); +ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn *value); +ScrStr *scr_wc_dom_string(const ScrDyn *value); +const char *scr_wc_key_format(const char *prefix, ScrDyn *value); +long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value); +void scr_wc_missing_member(const char *dict, const char *member); +ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key); +ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict); +const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *method); +const char *scr_wc_lookup(const char *const *names, size_t count, const ScrStr *name); +ScrDyn *scr_wc_cstr(const char *s); +ScrPromise *scr_wc_rejected(void); +bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required); +void scr_wc_args_take(ScrWcArgs *a, const ScrDyn *args, size_t n); +void scr_wc_args_drop(ScrWcArgs *a, size_t n); + +/* ECDSA, RSASSA-PKCS1-v1_5 and RSA-PSS (scr_webcrypto_pki.c, linked with + * the pki unit): each takes the method's argument vector (borrowed) and + * answers a +1 promise. NULL until that unit installs it. */ +typedef struct { + ScrPromise *(*generate_key)(const ScrDyn *args); + ScrPromise *(*import_key)(const ScrDyn *args); + ScrPromise *(*export_key)(const ScrDyn *args); + ScrPromise *(*sign)(const ScrDyn *args); + ScrPromise *(*verify)(const ScrDyn *args); + bool (*is_key)(const ScrDyn *value); +} ScrWcAsymmetric; + +extern const ScrWcAsymmetric *scr_wc_asymmetric; + +#endif diff --git a/packages/runtime/src/scr_webcrypto_pki.c b/packages/runtime/src/scr_webcrypto_pki.c new file mode 100644 index 0000000000..e913b1dc6b --- /dev/null +++ b/packages/runtime/src/scr_webcrypto_pki.c @@ -0,0 +1,845 @@ +/* WebCrypto's ECDSA, RSASSA-PKCS1-v1_5, RSA-PSS and Ed25519 algorithms: + * crypto.subtle.generateKey, importKey (spki, pkcs8, jwk, and raw for ECDSA + * and Ed25519 public keys), exportKey, sign and verify, and the CryptoKey + * values of these keys. ECDSA keys are on P-256, P-384 and P-521; ECDSA + * signatures are IEEE P1363 r || s. + * + * Argument conversion, algorithm normalization and the DOMException + * shapes follow Node 24's lib/internal/crypto/webcrypto.js, ec.js and + * rsa.js, through the Web IDL helpers of scr_webcrypto.c. The keys and + * signatures come from the pki unit (scr_pki.c). This unit installs + * itself into scr_webcrypto.c's dispatch table at startup; the compiler + * links it when a program uses WebCrypto and names one of these + * algorithms. All arguments are borrowed. */ +#include "scr_runtime.h" + +#include +#include +#include +#include + +#include "scr_pki.h" +#include "scr_webcrypto.h" + +typedef enum { SW_ECDSA, SW_RSASSA, SW_PSS, SW_ED25519 } SwKind; + +static const char *const sw_names[] = {"ECDSA", "RSASSA-PKCS1-v1_5", "RSA-PSS", "Ed25519"}; + +static const char *const sw_curves[] = {"P-256", "P-384", "P-521"}; + +/* ── CryptoKey ────────────────────────────────────────────────────────── */ + +typedef struct SwKey { + size_t rc; + ScrPkiKey *key; + SwKind kind; + const char *hash; /* RSA keys: the WebCrypto hash name */ + unsigned usages; + bool extractable; + ScrDyn *algorithm; /* cached getter results, or NULL */ + ScrDyn *usage_list; +} SwKey; + +static void *sw_key_retain(void *p) { + ((SwKey *)p)->rc++; + return p; +} + +static void sw_key_release(void *p) { + SwKey *k = p; + if (!k || --k->rc != 0) return; + scr_pki_key_release(k->key); + if (k->algorithm) scr_dyn_release(k->algorithm); + if (k->usage_list) scr_dyn_release(k->usage_list); + free(k); +} + +static SwKey *sw_key_of(const ScrDyn *value) { + if (value->kind != SCR_DYN_HANDLE || value->v.handle.tag != SCR_DYNH_ASYMMETRIC_CRYPTO_KEY) return NULL; + return value->v.handle.ptr; +} + +static bool sw_is_key(const ScrDyn *value) { return sw_key_of(value) != NULL; } + +static ScrDyn *sw_key_algorithm(SwKey *k) { + if (!k->algorithm) { + ScrDyn *algorithm = scr_dyn_new_obj(); + scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr(sw_names[k->kind])); + if (k->kind == SW_ED25519) { + /* { name } only */ + } else if (k->kind == SW_ECDSA) { + scr_dyn_obj_set(algorithm, "namedCurve", 10, scr_wc_cstr(scr_pki_key_curve(k->key))); + } else { + scr_dyn_obj_set(algorithm, "modulusLength", 13, scr_dyn_new_num((double)scr_pki_key_bits(k->key))); + ScrBytes *exponent = scr_pki_key_exponent(k->key); + scr_dyn_obj_set(algorithm, "publicExponent", 14, scr_dyn_new_bytes(exponent)); + scr_bytes_release(exponent); + ScrDyn *hash = scr_dyn_new_obj(); + scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(k->hash)); + scr_dyn_obj_set(algorithm, "hash", 4, hash); + } + k->algorithm = algorithm; + } + return scr_dyn_retain(k->algorithm); +} + +static ScrDyn *sw_usage_array(unsigned usages) { + ScrDyn *list = scr_dyn_new_arr(); + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) + if (usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i])); + return list; +} + +static ScrDyn *sw_key_usages(SwKey *k) { + if (!k->usage_list) k->usage_list = sw_usage_array(k->usages); + return scr_dyn_retain(k->usage_list); +} + +static bool sw_name_is(const char *name, size_t len, const char *literal) { + return len == strlen(literal) && memcmp(name, literal, len) == 0; +} + +static ScrDyn *sw_key_get(void *h, const char *name, size_t len) { + SwKey *k = h; + if (sw_name_is(name, len, "type")) + return scr_wc_cstr(scr_pki_key_is_private(k->key) ? "private" : "public"); + if (sw_name_is(name, len, "extractable")) return scr_dyn_new_bool(k->extractable); + if (sw_name_is(name, len, "algorithm")) return sw_key_algorithm(k); + if (sw_name_is(name, len, "usages")) return sw_key_usages(k); + return NULL; +} + +static bool sw_key_set(void *h, const char *name, size_t len, const ScrDyn *value) { + (void)h; + (void)value; + if (!sw_name_is(name, len, "type") && !sw_name_is(name, len, "extractable") && + !sw_name_is(name, len, "algorithm") && !sw_name_is(name, len, "usages")) + return false; + char message[128]; + int n = snprintf(message, sizeof message, "Cannot set property %.*s of # which has only a getter", + (int)len, name); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + return true; +} + +static ScrDyn *sw_key_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + (void)h; + (void)self; + (void)args; + (void)argc; + if (strcmp(method, "toString") == 0) return scr_wc_cstr("[object CryptoKey]"); + char message[160]; + int n = snprintf(message, sizeof message, "%s is not a function", what); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + return NULL; +} + +static ScrDyn *sw_key_symbol_get(void *h, const ScrDyn *key) { + (void)h; + if (key->kind != SCR_DYN_SYMBOL || !key->v.symbol.description) return NULL; + ScrStr *description = key->v.symbol.description(key->v.symbol.value); + bool tag = description && description->len == 18 && memcmp(description->data, "Symbol.toStringTag", 18) == 0; + if (description) scr_str_release(description); + return tag ? scr_wc_cstr("CryptoKey") : NULL; +} + +/* As for HMAC CryptoKeys, util.inspect refuses these keys rather than + * printing a different shape than Node's custom inspector. */ +static const ScrDynHandleOps sw_key_ops = { + .cls = "CryptoKey", + .retain = sw_key_retain, + .release = sw_key_release, + .invoke = sw_key_invoke, + .get = sw_key_get, + .set = sw_key_set, + .json_empty_object = true, + .to_string_tag = "CryptoKey", + .symbol_get = sw_key_symbol_get, +}; + +/* Boxes a CryptoKey over `key` (consumed). */ +static ScrDyn *sw_key_new(ScrPkiKey *key, SwKind kind, const char *hash, unsigned usages, bool extractable) { + static SCR_TL bool installed; + if (!installed) { + installed = true; + scr_dyn_handle_install(SCR_DYNH_ASYMMETRIC_CRYPTO_KEY, &sw_key_ops); + } + SwKey *k = calloc(1, sizeof *k); + if (!k) scr_trap("scriptc: out of memory\n"); + k->rc = 1; + k->key = key; + k->kind = kind; + k->hash = hash; + k->usages = usages; + k->extractable = extractable; + ScrDyn *box = scr_dyn_new_handle(k, SCR_DYNH_ASYMMETRIC_CRYPTO_KEY); + sw_key_release(k); + return box; +} + +/* ── algorithm parameters ─────────────────────────────────────────────── */ + +static SwKind sw_kind_of(const char *name) { + if (strcmp(name, "ECDSA") == 0) return SW_ECDSA; + if (strcmp(name, "Ed25519") == 0) return SW_ED25519; + return strcmp(name, "RSA-PSS") == 0 ? SW_PSS : SW_RSASSA; +} + +static bool sw_is_rsa_kind(SwKind kind) { return kind == SW_RSASSA || kind == SW_PSS; } + +/* The canonical algorithm name of an AlgorithmIdentifier the dispatcher + * already recognized as one of this unit's algorithms, or NULL with the + * error pending. */ +static const char *sw_algorithm_name(ScrDyn *identifier) { + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return NULL; + const char *found = scr_wc_lookup(sw_names, SCR_WC_COUNT(sw_names), name); + scr_str_release(name); + if (!found) scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return found; +} + +/* A required dictionary member (+1), or NULL with ERR_MISSING_OPTION. */ +static ScrDyn *sw_required(ScrDyn *plain, const char *dict, const char *member) { + ScrDyn *v = scr_wc_member(plain, member); + if (v->kind == SCR_DYN_UNDEF) { + scr_dyn_release(v); + scr_wc_missing_member(dict, member); + return NULL; + } + return v; +} + +/* namedCurve (DOMString, required): one of the implemented curves, or + * NULL with the error pending. */ +static const char *sw_named_curve(ScrDyn *plain, const char *dict) { + ScrDyn *value = sw_required(plain, dict, "namedCurve"); + if (!value) return NULL; + ScrStr *s = scr_wc_dom_string(value); + scr_dyn_release(value); + if (!s) return NULL; + const char *curve = NULL; + for (size_t i = 0; i < SCR_WC_COUNT(sw_curves); i++) + if (s->len == strlen(sw_curves[i]) && memcmp(s->data, sw_curves[i], s->len) == 0) curve = sw_curves[i]; + scr_str_release(s); + if (!curve) scr_throw_domex("NotSupportedError", "Unrecognized namedCurve"); + return curve; +} + +/* hash (HashAlgorithmIdentifier, required): with `sha_only`, Node's + * ensureSHA check first ("Only SHA hashes are supported in "). */ +static const char *sw_hash(ScrDyn *plain, const char *dict, bool sha_only, const char *method) { + ScrDyn *hash = sw_required(plain, dict, "hash"); + if (!hash) return NULL; + if (sha_only) { + ScrStr *name = NULL; + if (scr_wc_is_object(hash)) { + ScrDyn *hp = scr_wc_plain(hash); + ScrDyn *member = hp ? scr_wc_member(hp, "name") : NULL; + if (hp) scr_dyn_release(hp); + if (member && member->kind == SCR_DYN_STR) name = scr_str_retain(member->v.str); + if (member) scr_dyn_release(member); + } else { + name = scr_wc_dom_string(hash); + if (!name) { + scr_dyn_release(hash); + return NULL; + } + } + bool sha = name && name->len >= 3 && (name->data[0] | 32) == 's' && (name->data[1] | 32) == 'h' && + (name->data[2] | 32) == 'a'; + if (name) scr_str_release(name); + if (!sha) { + char message[96]; + snprintf(message, sizeof message, "Only SHA hashes are supported in %s", dict); + scr_throw_domex("NotSupportedError", message); + scr_dyn_release(hash); + return NULL; + } + } + const char *canonical = scr_wc_digest_algorithm(hash, method); + scr_dyn_release(hash); + return canonical; +} + +/* An [EnforceRange] unsigned long member; false with the TypeError + * pending. */ +static bool sw_unsigned_long(ScrDyn *value, const char *member, double *out) { + double x; + if (!scr_dyn_number_coerce_js(value, &x)) return false; + if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: %s in passed algorithm is not a finite number.", + scr_wc_normalize_prefix, member); + return false; + } + x = trunc(x); + if (x < 0 || x > 4294967295.0) { + scr_wc_type_error("ERR_OUT_OF_RANGE", + "%s: %s in passed algorithm is outside the expected range of 0 to 4294967295.", + scr_wc_normalize_prefix, member); + return false; + } + *out = x == 0 ? 0 : x; + return true; +} + +static void sw_unsupported_usage(SwKind kind) { + char message[96]; + snprintf(message, sizeof message, "Unsupported key usage for %s key", sw_names[kind]); + scr_throw_domex("SyntaxError", message); +} + +/* ── generateKey ──────────────────────────────────────────────────────── */ + +static ScrPromise *sw_generate_key(const ScrDyn *args) { + static const char prefix[] = "Failed to execute 'generateKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 3)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 3); + ScrDyn *pair = NULL; + ScrDyn *plain = NULL; + if (!scr_wc_is_object(a.v[0])) { + ScrStr *s = scr_wc_dom_string(a.v[0]); + if (!s) goto done; + scr_str_release(s); + } + bool extractable = scr_dyn_truthy(a.v[1]); + long usages = scr_wc_key_usages(prefix, 2, a.v[2]); + if (usages < 0) goto done; + const char *name = sw_algorithm_name(a.v[0]); + if (!name) goto done; + SwKind kind = sw_kind_of(name); + plain = scr_wc_plain(a.v[0]); + if (!plain) goto done; + const char *curve = NULL; + const char *hash = NULL; + double bits = 0; + ScrBytes *exponent = NULL; + if (kind == SW_ED25519) { + /* no parameters */ + } else if (kind == SW_ECDSA) { + if (!scr_wc_is_object(plain)) { + scr_wc_missing_member("EcKeyGenParams", "namedCurve"); + goto done; + } + curve = sw_named_curve(plain, "EcKeyGenParams"); + if (!curve) goto done; + } else { + static const char dict[] = "RsaHashedKeyGenParams"; + if (!scr_wc_is_object(plain)) { + scr_wc_missing_member(dict, "hash"); + goto done; + } + /* Dictionary members convert in name order: hash, modulusLength, + * publicExponent. */ + hash = sw_hash(plain, dict, true, "generateKey"); + if (!hash) goto done; + ScrDyn *length = sw_required(plain, dict, "modulusLength"); + if (!length) goto done; + bool ok = sw_unsigned_long(length, "modulusLength", &bits); + scr_dyn_release(length); + if (!ok) goto done; + ScrDyn *e = sw_required(plain, dict, "publicExponent"); + if (!e) goto done; + if (e->kind != SCR_DYN_BYTES || e->v.bytes->elem != SCR_BYTES_U8) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: publicExponent in passed algorithm is not instance of Uint8Array.", + scr_wc_normalize_prefix); + scr_dyn_release(e); + goto done; + } + exponent = scr_bytes_buffer_source(e); + scr_dyn_release(e); + } + if (usages & ~(long)(SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY)) { + sw_unsupported_usage(kind); + if (exponent) scr_bytes_release(exponent); + goto done; + } + ScrPkiKey *key; + if (kind == SW_ED25519) { + key = scr_pki_generate_ed25519(); + if (!key) goto done; + } else if (kind == SW_ECDSA) { + key = scr_pki_generate_ec(curve); + } else { + /* publicExponent: big-endian, at most a 32-bit value. */ + size_t n = (size_t)scr_bytes_byte_len(exponent); + uint64_t value = 0; + bool fits = true; + for (size_t i = 0; i < n; i++) { + if (value > 0xffffffu && fits) fits = false; + value = (value << 8) | exponent->data[i]; + } + scr_bytes_release(exponent); + if (!fits || value > 0xffffffffu) { + scr_throw_domex("OperationError", "The publicExponent must be equivalent to an unsigned 32-bit value"); + goto done; + } + if (bits < 512) { + scr_throw_domex("OperationError", "algorithm.modulusLength must be at least 512"); + goto done; + } + if ((value & 1) == 0) { + scr_throw_domex("OperationError", "algorithm.publicExponent must be odd"); + goto done; + } + if (bits > 16384 || value < 3) { + scr_wc_unsupported("generateKey", "this RSA modulus length or public exponent"); + goto done; + } + key = scr_pki_generate_rsa((unsigned)bits, value); + } + if (!key) { + scr_throw_domex("OperationError", "Key generation failed"); + goto done; + } + unsigned private_usages = (unsigned)usages & SCR_WC_USAGE_SIGN; + unsigned public_usages = (unsigned)usages & SCR_WC_USAGE_VERIFY; + if (private_usages == 0) { + scr_pki_key_release(key); + scr_throw_domex("SyntaxError", "Usages cannot be empty when creating a key."); + goto done; + } + ScrPkiKey *public_key = scr_pki_key_public_of(key); + if (!public_key) { + scr_pki_key_release(key); + scr_throw_domex("OperationError", "Key generation failed"); + goto done; + } + pair = scr_dyn_new_obj(); + scr_dyn_obj_set(pair, "publicKey", 9, sw_key_new(public_key, kind, hash, public_usages, true)); + scr_dyn_obj_set(pair, "privateKey", 10, sw_key_new(key, kind, hash, private_usages, extractable)); +done: + if (plain) scr_dyn_release(plain); + scr_wc_args_drop(&a, 3); + if (!pair) return scr_wc_rejected(); + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_dyn(pair, NULL, scr_crypto_promise_callback(p), scr_crypto_promise_settle_dyn); + return p; +} + +/* ── importKey ────────────────────────────────────────────────────────── */ + +/* A JWK string member, or NULL. */ +static const ScrDyn *sw_jwk_member(const ScrDyn *jwk, const char *key) { + const ScrDyn *v = scr_dyn_obj_get(jwk, key, strlen(key)); + return v && v->kind != SCR_DYN_UNDEF ? v : NULL; +} + +static bool sw_jwk_is(const ScrDyn *v, const char *text) { + return v && v->kind == SCR_DYN_STR && v->v.str->len == strlen(text) && + memcmp(v->v.str->data, text, v->v.str->len) == 0; +} + +/* The JWK checks Node runs before reading the key: use, key_ops and ext. */ +static bool sw_jwk_checks(const ScrDyn *jwk, unsigned usages, bool extractable) { + const ScrDyn *use = sw_jwk_member(jwk, "use"); + if (use && !sw_jwk_is(use, "sig")) { + scr_throw_domex("DataError", "Invalid JWK \"use\" Parameter"); + return false; + } + const ScrDyn *ops = sw_jwk_member(jwk, "key_ops"); + if (ops) { + if (ops->kind != SCR_DYN_ARR) { + scr_throw_domex("DataError", "Key operations and usage mismatch"); + return false; + } + for (size_t u = 0; u < SCR_WC_COUNT(scr_wc_usages); u++) { + if (!(usages & (1u << u))) continue; + bool listed = false; + double n = scr_dyn_arr_len(ops); + for (double i = 0; i < n && !listed; i++) { + ScrDyn *item = scr_dyn_arr_at(ops, i); + listed = sw_jwk_is(item, scr_wc_usages[u]); + scr_dyn_release(item); + } + if (!listed) { + scr_throw_domex("DataError", "Key operations and usage mismatch"); + return false; + } + } + } + const ScrDyn *ext = sw_jwk_member(jwk, "ext"); + if (ext && ext->kind == SCR_DYN_BOOL && !ext->v.b && extractable) { + scr_throw_domex("DataError", "JWK \"ext\" Parameter and extractable mismatch"); + return false; + } + return true; +} + +/* The JWK "alg" an RSA key with this hash carries ("RS256", "PS384"). */ +static void sw_rsa_jwk_alg(SwKind kind, const char *hash, char out[8]) { + const char *bits = strcmp(hash, "SHA-1") == 0 ? "1" : hash + 4; + snprintf(out, 8, "%s%s", kind == SW_PSS ? "PS" : "RS", bits); +} + +static ScrDyn *sw_import(const ScrDyn *args) { + static const char prefix[] = "Failed to execute 'importKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 5)) return NULL; + ScrWcArgs a; + scr_wc_args_take(&a, args, 5); + ScrDyn *result = NULL; + ScrBytes *data = NULL; + ScrDyn *jwk = NULL; + ScrDyn *plain = NULL; + const char *format = scr_wc_key_format(prefix, a.v[0]); + if (!format) goto done; + bool is_jwk = strcmp(format, "jwk") == 0; + if (is_jwk) { + if (!scr_wc_is_object(a.v[1])) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type JsonWebKey.", prefix); + goto done; + } + jwk = scr_wc_plain(a.v[1]); + if (!jwk) goto done; + } else { + data = scr_wc_buffer_source(prefix, 1, a.v[1]); + if (!data) goto done; + } + if (!scr_wc_is_object(a.v[2])) { + ScrStr *s = scr_wc_dom_string(a.v[2]); + if (!s) goto done; + scr_str_release(s); + } + bool extractable = scr_dyn_truthy(a.v[3]); + long usages = scr_wc_key_usages(prefix, 4, a.v[4]); + if (usages < 0) goto done; + const char *name = sw_algorithm_name(a.v[2]); + if (!name) goto done; + SwKind kind = sw_kind_of(name); + plain = scr_wc_plain(a.v[2]); + if (!plain) goto done; + const char *curve = NULL; + const char *hash = NULL; + if (kind == SW_ED25519) { + /* no parameters */ + } else if (kind == SW_ECDSA) { + if (!scr_wc_is_object(plain)) { + scr_wc_missing_member("EcKeyImportParams", "namedCurve"); + goto done; + } + curve = sw_named_curve(plain, "EcKeyImportParams"); + if (!curve) goto done; + } else { + if (!scr_wc_is_object(plain)) { + scr_wc_missing_member("RsaHashedImportParams", "hash"); + goto done; + } + hash = sw_hash(plain, "RsaHashedImportParams", true, "importKey"); + if (!hash) goto done; + } + bool private_key; + if (is_jwk) { + private_key = sw_jwk_member(jwk, "d") != NULL; + } else if (strcmp(format, "pkcs8") == 0) { + private_key = true; + } else if (strcmp(format, "spki") == 0 || + ((kind == SW_ECDSA || kind == SW_ED25519) && strcmp(format, "raw") == 0)) { + private_key = false; + } else { + char message[96]; + snprintf(message, sizeof message, "Unable to import %s using %s format", sw_names[kind], format); + scr_throw_domex("NotSupportedError", message); + goto done; + } + unsigned allowed = private_key ? SCR_WC_USAGE_SIGN : SCR_WC_USAGE_VERIFY; + ScrPkiKey *key = NULL; + /* An Ed25519 private key derives its public key in the Ed25519 unit. */ + if (kind == SW_ED25519 && private_key && !scr_pki_ed25519_ready()) goto done; + if (is_jwk) { + const ScrDyn *kty = sw_jwk_member(jwk, "kty"); + if (!kty) { + scr_throw_domex("DataError", "Invalid keyData"); + goto done; + } + if (!sw_jwk_is(kty, kind == SW_ECDSA ? "EC" : kind == SW_ED25519 ? "OKP" : "RSA")) { + scr_throw_domex("DataError", "Invalid JWK \"kty\" Parameter"); + goto done; + } + if (kind == SW_ECDSA && !sw_jwk_is(sw_jwk_member(jwk, "crv"), curve)) { + scr_throw_domex("DataError", "JWK \"crv\" does not match the requested algorithm"); + goto done; + } + if (kind == SW_ED25519 && !sw_jwk_is(sw_jwk_member(jwk, "crv"), "Ed25519")) { + scr_throw_domex("DataError", "JWK \"crv\" Parameter and algorithm name mismatch"); + goto done; + } + if ((unsigned)usages & ~allowed) { + sw_unsupported_usage(kind); + goto done; + } + if (!sw_jwk_checks(jwk, (unsigned)usages, extractable)) goto done; + const ScrDyn *alg = sw_jwk_member(jwk, "alg"); + if (kind == SW_ED25519 && alg && !sw_jwk_is(alg, "Ed25519") && !sw_jwk_is(alg, "EdDSA")) { + scr_throw_domex("DataError", "JWK \"alg\" does not match the requested algorithm"); + goto done; + } + if (sw_is_rsa_kind(kind) && alg) { + char expected[8]; + sw_rsa_jwk_alg(kind, hash, expected); + if (!sw_jwk_is(alg, expected)) { + scr_throw_domex("DataError", "JWK \"alg\" does not match the requested algorithm"); + goto done; + } + } + key = scr_pki_key_from_jwk(jwk, private_key); + } else { + if ((unsigned)usages & ~allowed) { + sw_unsupported_usage(kind); + goto done; + } + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + if (strcmp(format, "raw") == 0) + key = kind == SW_ED25519 ? scr_pki_key_from_ed25519_raw(view->data, len) + : scr_pki_key_from_point(curve, view->data, len); + else key = scr_pki_key_parse_der(view->data, len, private_key); + } + if (!key) { + scr_throw_domex("DataError", "Invalid keyData"); + goto done; + } + bool key_matches = kind == SW_ED25519 ? scr_pki_key_is_ed25519(key) + : kind == SW_ECDSA ? !scr_pki_key_is_rsa(key) && !scr_pki_key_is_ed25519(key) + : scr_pki_key_is_rsa(key); + if (!key_matches) { + scr_pki_key_release(key); + scr_throw_domex("DataError", "Invalid key type"); + goto done; + } + if (kind == SW_ECDSA && strcmp(scr_pki_key_curve(key), curve) != 0) { + scr_pki_key_release(key); + scr_throw_domex("DataError", "Named curve mismatch"); + goto done; + } + if (private_key && usages == 0) { + scr_pki_key_release(key); + scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a private key."); + goto done; + } + result = sw_key_new(key, kind, hash, (unsigned)usages, extractable); +done: + if (data) scr_bytes_release(data); + if (jwk) scr_dyn_release(jwk); + if (plain) scr_dyn_release(plain); + scr_wc_args_drop(&a, 5); + return result; +} + +static ScrPromise *sw_import_key(const ScrDyn *args) { + ScrDyn *key = sw_import(args); + if (!key) return scr_wc_rejected(); + return scr_promise_settled_ref(key, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); +} + +/* ── exportKey ────────────────────────────────────────────────────────── */ + +static ScrDyn *sw_array_buffer(ScrBytes *bytes) { + ScrDyn *out = scr_array_buffer_from_bytes(bytes); + scr_bytes_release(bytes); + return out; +} + +static ScrDyn *sw_export(const ScrDyn *args) { + ScrWcArgs a; + scr_wc_args_take(&a, args, 2); + ScrDyn *result = NULL; + const char *format = scr_wc_key_format("Failed to execute 'exportKey' on 'SubtleCrypto'", a.v[0]); + SwKey *k = sw_key_of(a.v[1]); + bool private_key = scr_pki_key_is_private(k->key); + if (!format) goto done; + if (!k->extractable) { + scr_throw_domex("InvalidAccessError", "key is not extractable"); + goto done; + } + if (strcmp(format, "spki") == 0 || strcmp(format, "pkcs8") == 0) { + bool want_private = strcmp(format, "pkcs8") == 0; + if (want_private != private_key) { + scr_throw_domex("InvalidAccessError", want_private ? "Key must be a private key" : "Key must be a public key"); + goto done; + } + ScrBytes *der = scr_pki_export_der(k->key); + if (!der) { + scr_throw_domex("OperationError", "Failed to export key"); + goto done; + } + result = sw_array_buffer(der); + } else if (strcmp(format, "raw") == 0 && (k->kind == SW_ECDSA || k->kind == SW_ED25519) && + !private_key) { + ScrBytes *point = k->kind == SW_ED25519 ? scr_pki_export_ed25519_raw(k->key) : scr_pki_export_point(k->key); + if (!point) { + scr_throw_domex("OperationError", "Failed to export key"); + goto done; + } + result = sw_array_buffer(point); + } else if (strcmp(format, "jwk") == 0) { + ScrDyn *members = scr_pki_export_jwk(k->key); + ScrDyn *jwk = scr_dyn_new_obj(); + scr_dyn_obj_set(jwk, "key_ops", 7, sw_usage_array(k->usages)); + scr_dyn_obj_set(jwk, "ext", 3, scr_dyn_new_bool(k->extractable)); + if (sw_is_rsa_kind(k->kind)) { + char alg[8]; + sw_rsa_jwk_alg(k->kind, k->hash, alg); + scr_dyn_obj_set(jwk, "alg", 3, scr_wc_cstr(alg)); + } else if (k->kind == SW_ED25519) { + scr_dyn_obj_set(jwk, "alg", 3, scr_wc_cstr("Ed25519")); + } + /* The key members in Node's order for each key type. */ + static const char *const rsa_names[] = {"kty", "n", "e", "d", "p", "q", "dp", "dq", "qi"}; + static const char *const ec_names[] = {"kty", "x", "y", "crv", "d"}; + static const char *const okp_names[] = {"kty", "crv", "x", "d"}; + const char *const *names = sw_is_rsa_kind(k->kind) ? rsa_names : k->kind == SW_ED25519 ? okp_names : ec_names; + size_t count = sw_is_rsa_kind(k->kind) ? SCR_WC_COUNT(rsa_names) + : k->kind == SW_ED25519 ? SCR_WC_COUNT(okp_names) + : SCR_WC_COUNT(ec_names); + for (size_t i = 0; i < count; i++) { + const ScrDyn *v = scr_dyn_obj_get(members, names[i], strlen(names[i])); + if (v) scr_dyn_obj_set(jwk, names[i], strlen(names[i]), scr_dyn_retain((ScrDyn *)v)); + } + scr_dyn_release(members); + result = jwk; + } else { + char message[128]; + snprintf(message, sizeof message, "Unable to export %s %s key using %s format", sw_names[k->kind], + private_key ? "private" : "public", format); + scr_throw_domex("NotSupportedError", message); + } +done: + scr_wc_args_drop(&a, 2); + return result; +} + +static ScrPromise *sw_export_key(const ScrDyn *args) { + ScrDyn *value = sw_export(args); + if (!value) return scr_wc_rejected(); + return scr_promise_settled_ref(value, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); +} + +/* ── sign and verify ──────────────────────────────────────────────────── */ + +/* The deferred verify result: a dyn boolean. */ +static void sw_settle_verified(ScrClosure *cb, ScrError *error, ScrBytes *value) { + (void)error; + ScrPromise *p; + memcpy(&p, &cb->caps[0]->slot, sizeof p); + ScrDyn *verified = (ScrDyn *)value; + scr_promise_fulfill_bool(p, scr_dyn_truthy(verified)); + scr_dyn_release(verified); +} + +static ScrPromise *sw_sign_verify(const ScrDyn *args, bool verify) { + const char *prefix = verify ? "Failed to execute 'verify' on 'SubtleCrypto'" + : "Failed to execute 'sign' on 'SubtleCrypto'"; + const char *method = verify ? "verify" : "sign"; + size_t n = verify ? 4 : 3; + if (!scr_wc_required(prefix, args, n)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, n); + ScrPromise *result = NULL; + ScrBytes *data = NULL; + ScrBytes *signature = NULL; + ScrDyn *plain = NULL; + if (!scr_wc_is_object(a.v[0])) { + ScrStr *s = scr_wc_dom_string(a.v[0]); + if (!s) goto done; + scr_str_release(s); + } + SwKey *k = sw_key_of(a.v[1]); + if (!k && !scr_webcrypto_is_crypto_key(a.v[1])) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + if (verify) { + signature = scr_wc_buffer_source(prefix, 2, a.v[2]); + if (!signature) goto done; + } + data = scr_wc_buffer_source(prefix, verify ? 3 : 2, a.v[verify ? 3 : 2]); + if (!data) goto done; + const char *name = sw_algorithm_name(a.v[0]); + if (!name) goto done; + SwKind kind = sw_kind_of(name); + plain = scr_wc_plain(a.v[0]); + if (!plain) goto done; + const char *hash = NULL; + int salt = 0; + if (kind == SW_ED25519) { + /* no parameters */ + } else if (kind == SW_ECDSA) { + if (!scr_wc_is_object(plain)) { + scr_wc_missing_member("EcdsaParams", "hash"); + goto done; + } + hash = sw_hash(plain, "EcdsaParams", false, method); + if (!hash) goto done; + } else if (kind == SW_PSS) { + if (!scr_wc_is_object(plain)) { + scr_wc_missing_member("RsaPssParams", "saltLength"); + goto done; + } + ScrDyn *value = sw_required(plain, "RsaPssParams", "saltLength"); + if (!value) goto done; + double x; + bool ok = sw_unsigned_long(value, "saltLength", &x); + scr_dyn_release(value); + if (!ok) goto done; + salt = x > 65536 ? 65536 : (int)x; + } + if (!k || k->kind != kind) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + goto done; + } + if (!(k->usages & (verify ? SCR_WC_USAGE_VERIFY : SCR_WC_USAGE_SIGN))) { + char message[64]; + snprintf(message, sizeof message, "Unable to use this key to %s", method); + scr_throw_domex("InvalidAccessError", message); + goto done; + } + if (sw_is_rsa_kind(kind)) hash = k->hash; + if (kind == SW_ED25519 && !scr_pki_ed25519_ready()) goto done; + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + if (verify) { + ScrBytes *sig = signature; + SCR_BYTES_SNAPSHOT(sig); + bool ok = scr_pki_verify(k->key, hash, kind == SW_PSS, salt, view->data, len, sig->data, + (size_t)scr_bytes_byte_len(sig)); + result = scr_promise_new(); + scr_crypto_defer_dyn(scr_dyn_new_bool(ok), NULL, scr_crypto_promise_callback(result), sw_settle_verified); + } else { + ScrBytes *out = scr_pki_sign(k->key, hash, kind == SW_PSS, salt, view->data, len); + if (!out) { + scr_throw_domex("OperationError", "Failed to sign"); + goto done; + } + result = scr_promise_new(); + scr_crypto_defer_bytes(out, NULL, scr_crypto_promise_callback(result), scr_crypto_promise_settle_array_buffer); + } + } +done: + if (data) scr_bytes_release(data); + if (signature) scr_bytes_release(signature); + if (plain) scr_dyn_release(plain); + scr_wc_args_drop(&a, n); + return result ? result : scr_wc_rejected(); +} + +static ScrPromise *sw_sign(const ScrDyn *args) { return sw_sign_verify(args, false); } + +static ScrPromise *sw_verify(const ScrDyn *args) { return sw_sign_verify(args, true); } + +static const ScrWcAsymmetric sw_table = { + .generate_key = sw_generate_key, + .import_key = sw_import_key, + .export_key = sw_export_key, + .sign = sw_sign, + .verify = sw_verify, + .is_key = sw_is_key, +}; + +/* Installed before main: scr_webcrypto.c dispatches these algorithms + * here. */ +__attribute__((constructor)) static void sw_install(void) { scr_wc_asymmetric = &sw_table; } diff --git a/packages/runtime/vendor/README.md b/packages/runtime/vendor/README.md index 977179609b..7ab8cf6b00 100644 --- a/packages/runtime/vendor/README.md +++ b/packages/runtime/vendor/README.md @@ -48,3 +48,15 @@ The objects build lazily on the first zlib-using cross compile into the per-user The TLS provider behind node:tls/node:https (see the design note atop packages/runtime/src/scr_tls.c for why mbedTLS over SecureTransport/BoringSSL/libtls). Only `include/` and `library/` are vendored (LICENSE beside them) — tests, docs, programs, scripts, and CMake machinery are not. No vendored file is modified and no custom config is applied (the stock `mbedtls_config.h` builds every `library/*.c` standalone with clang); to update, re-fetch the release tarball, copy the same two directories, and bump `MBEDTLS_VERSION` in packages/compiler/src/backend/vendor-archives.ts. The archive (libmbedtls.a) is prebuilt best-effort during npm installation (or explicitly by `scriptc cache warm`) and otherwise built lazily on the first TLS-using compile. It lives under the per-user build cache's `vendor/mbedtls----/` directory — one flavor per lane (plain, asan), native platform/architecture or explicit cross target, and compiler environment/identity, the libqjs.a pattern. TLS-free binaries never compile or link any of this. + +## libsodium/ + +- Project: https://github.com/jedisct1/libsodium +- Version: 1.0.20 (release tarball libsodium-1.0.20.tar.gz, sha256 ebb65ef6ca439333c2bb41a0c1990587288da07f6c7fd07cb3a18cc18d30ce19) +- License: ISC (see libsodium/LICENSE) + +The Ed25519 arithmetic behind node:crypto and WebCrypto Ed25519 keys (see packages/runtime/src/scr_ed25519.c). Its verification matches Node 24: it rejects a non-canonical S and the small-order R and public-key encodings, then checks the cofactorless equation. Only the ref10 Ed25519 subset is vendored, flat in one directory: `crypto_core/ed25519/ref10/ed25519_ref10.c` with its `fe_25_5/` headers, `crypto_sign/ed25519/ref10/{sign,open,keypair}.c` and `sign_ed25519_ref10.h`, `crypto_hash/sha512/cp/hash_sha512_cp.c`, `crypto_verify/verify.c`, and the public and private headers they include. The 64-bit `fe_51` field code (used only with `HAVE_TI_MODE`) and the rest of the library are not vendored. + +The only modification to upstream files: `#include "private/x.h"` flattened to `#include "x.h"` in ed25519_ref10.c, sign.c, open.c, keypair.c, hash_sha512_cp.c and ed25519_ref10_fe_25_5.h, so the directory is self-contained. scr_ed25519.c textually includes the .c files and supplies `sodium_memzero`, `sodium_memcmp`, `sodium_is_zero` and `randombytes_buf` (from `arc4random_buf`) in place of libsodium's utils.c and randombytes.c. To update, re-fetch the release tarball, copy the same files, and re-apply that include flattening. + +The unit compiles only into programs that use asymmetric keys and name Ed25519 in a string literal (the `ed25519` runtime feature); other binaries never compile or link any of this. diff --git a/packages/runtime/vendor/libsodium/LICENSE b/packages/runtime/vendor/libsodium/LICENSE new file mode 100644 index 0000000000..6bce7f0581 --- /dev/null +++ b/packages/runtime/vendor/libsodium/LICENSE @@ -0,0 +1,18 @@ +/* + * ISC License + * + * Copyright (c) 2013-2024 + * Frank Denis + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/packages/runtime/vendor/libsodium/README.md b/packages/runtime/vendor/libsodium/README.md new file mode 100644 index 0000000000..5207d8ad67 --- /dev/null +++ b/packages/runtime/vendor/libsodium/README.md @@ -0,0 +1,7 @@ +# libsodium 1.0.20 (Ed25519 subset) + +- Source: https://download.libsodium.org/libsodium/releases/libsodium-1.0.20.tar.gz (the same tarball as the GitHub release 1.0.20-RELEASE) +- sha256: ebb65ef6ca439333c2bb41a0c1990587288da07f6c7fd07cb3a18cc18d30ce19 +- License: ISC, see LICENSE + +See ../README.md for the files vendored, the one modification, and how to update. diff --git a/packages/runtime/vendor/libsodium/common.h b/packages/runtime/vendor/libsodium/common.h new file mode 100644 index 0000000000..63f0d2b2dc --- /dev/null +++ b/packages/runtime/vendor/libsodium/common.h @@ -0,0 +1,285 @@ +#ifndef common_H +#define common_H 1 + +#if !defined(_MSC_VER) && !defined(DEV_MODE) && 0 +# warning *** This is unstable, untested, development code. +# warning It might not compile. It might not work as expected. +# warning It might be totally insecure. +# warning Do not use this except if you are planning to contribute code. +# warning Use releases available at https://download.libsodium.org/libsodium/releases/ instead. +# warning Alternatively, use the "stable" branch in the git repository. +#endif + +#if !defined(_MSC_VER) && (!defined(CONFIGURED) || CONFIGURED != 1) +# warning *** The library is being compiled using an undocumented method. +# warning This is not supported. It has not been tested, it might not +# warning work as expected, and performance is likely to be suboptimal. +#endif + +#include +#include +#include + +#define COMPILER_ASSERT(X) (void) sizeof(char[(X) ? 1 : -1]) + +#ifdef HAVE_TI_MODE +# if defined(__SIZEOF_INT128__) +typedef unsigned __int128 uint128_t; +# else +typedef unsigned uint128_t __attribute__((mode(TI))); +# endif +#endif + +#define ROTL32(X, B) rotl32((X), (B)) +static inline uint32_t +rotl32(const uint32_t x, const int b) +{ + return (x << b) | (x >> (32 - b)); +} + +#define ROTL64(X, B) rotl64((X), (B)) +static inline uint64_t +rotl64(const uint64_t x, const int b) +{ + return (x << b) | (x >> (64 - b)); +} + +#define ROTR32(X, B) rotr32((X), (B)) +static inline uint32_t +rotr32(const uint32_t x, const int b) +{ + return (x >> b) | (x << (32 - b)); +} + +#define ROTR64(X, B) rotr64((X), (B)) +static inline uint64_t +rotr64(const uint64_t x, const int b) +{ + return (x >> b) | (x << (64 - b)); +} + +#define LOAD64_LE(SRC) load64_le(SRC) +static inline uint64_t +load64_le(const uint8_t src[8]) +{ +#ifdef NATIVE_LITTLE_ENDIAN + uint64_t w; + memcpy(&w, src, sizeof w); + return w; +#else + uint64_t w = (uint64_t) src[0]; + w |= (uint64_t) src[1] << 8; + w |= (uint64_t) src[2] << 16; + w |= (uint64_t) src[3] << 24; + w |= (uint64_t) src[4] << 32; + w |= (uint64_t) src[5] << 40; + w |= (uint64_t) src[6] << 48; + w |= (uint64_t) src[7] << 56; + return w; +#endif +} + +#define STORE64_LE(DST, W) store64_le((DST), (W)) +static inline void +store64_le(uint8_t dst[8], uint64_t w) +{ +#ifdef NATIVE_LITTLE_ENDIAN + memcpy(dst, &w, sizeof w); +#else + dst[0] = (uint8_t) w; w >>= 8; + dst[1] = (uint8_t) w; w >>= 8; + dst[2] = (uint8_t) w; w >>= 8; + dst[3] = (uint8_t) w; w >>= 8; + dst[4] = (uint8_t) w; w >>= 8; + dst[5] = (uint8_t) w; w >>= 8; + dst[6] = (uint8_t) w; w >>= 8; + dst[7] = (uint8_t) w; +#endif +} + +#define LOAD32_LE(SRC) load32_le(SRC) +static inline uint32_t +load32_le(const uint8_t src[4]) +{ +#ifdef NATIVE_LITTLE_ENDIAN + uint32_t w; + memcpy(&w, src, sizeof w); + return w; +#else + uint32_t w = (uint32_t) src[0]; + w |= (uint32_t) src[1] << 8; + w |= (uint32_t) src[2] << 16; + w |= (uint32_t) src[3] << 24; + return w; +#endif +} + +#define STORE32_LE(DST, W) store32_le((DST), (W)) +static inline void +store32_le(uint8_t dst[4], uint32_t w) +{ +#ifdef NATIVE_LITTLE_ENDIAN + memcpy(dst, &w, sizeof w); +#else + dst[0] = (uint8_t) w; w >>= 8; + dst[1] = (uint8_t) w; w >>= 8; + dst[2] = (uint8_t) w; w >>= 8; + dst[3] = (uint8_t) w; +#endif +} + +/* ----- */ + +#define LOAD64_BE(SRC) load64_be(SRC) +static inline uint64_t +load64_be(const uint8_t src[8]) +{ +#ifdef NATIVE_BIG_ENDIAN + uint64_t w; + memcpy(&w, src, sizeof w); + return w; +#else + uint64_t w = (uint64_t) src[7]; + w |= (uint64_t) src[6] << 8; + w |= (uint64_t) src[5] << 16; + w |= (uint64_t) src[4] << 24; + w |= (uint64_t) src[3] << 32; + w |= (uint64_t) src[2] << 40; + w |= (uint64_t) src[1] << 48; + w |= (uint64_t) src[0] << 56; + return w; +#endif +} + +#define STORE64_BE(DST, W) store64_be((DST), (W)) +static inline void +store64_be(uint8_t dst[8], uint64_t w) +{ +#ifdef NATIVE_BIG_ENDIAN + memcpy(dst, &w, sizeof w); +#else + dst[7] = (uint8_t) w; w >>= 8; + dst[6] = (uint8_t) w; w >>= 8; + dst[5] = (uint8_t) w; w >>= 8; + dst[4] = (uint8_t) w; w >>= 8; + dst[3] = (uint8_t) w; w >>= 8; + dst[2] = (uint8_t) w; w >>= 8; + dst[1] = (uint8_t) w; w >>= 8; + dst[0] = (uint8_t) w; +#endif +} + +#define LOAD32_BE(SRC) load32_be(SRC) +static inline uint32_t +load32_be(const uint8_t src[4]) +{ +#ifdef NATIVE_BIG_ENDIAN + uint32_t w; + memcpy(&w, src, sizeof w); + return w; +#else + uint32_t w = (uint32_t) src[3]; + w |= (uint32_t) src[2] << 8; + w |= (uint32_t) src[1] << 16; + w |= (uint32_t) src[0] << 24; + return w; +#endif +} + +#define STORE32_BE(DST, W) store32_be((DST), (W)) +static inline void +store32_be(uint8_t dst[4], uint32_t w) +{ +#ifdef NATIVE_BIG_ENDIAN + memcpy(dst, &w, sizeof w); +#else + dst[3] = (uint8_t) w; w >>= 8; + dst[2] = (uint8_t) w; w >>= 8; + dst[1] = (uint8_t) w; w >>= 8; + dst[0] = (uint8_t) w; +#endif +} + +#define XOR_BUF(OUT, IN, N) xor_buf((OUT), (IN), (N)) +static inline void +xor_buf(unsigned char *out, const unsigned char *in, size_t n) +{ + size_t i; + + for (i = 0; i < n; i++) { + out[i] ^= in[i]; + } +} + +#if !defined(__clang__) && !defined(__GNUC__) +# ifdef __attribute__ +# undef __attribute__ +# endif +# define __attribute__(a) +#endif + +#ifndef CRYPTO_ALIGN +# if defined(__INTEL_COMPILER) || defined(_MSC_VER) +# define CRYPTO_ALIGN(x) __declspec(align(x)) +# else +# define CRYPTO_ALIGN(x) __attribute__ ((aligned(x))) +# endif +#endif + +#ifdef _MSC_VER + +# if defined(_M_X64) || defined(_M_IX86) +# include + +# define HAVE_INTRIN_H 1 +# define HAVE_MMINTRIN_H 1 +# define HAVE_EMMINTRIN_H 1 +# define HAVE_PMMINTRIN_H 1 +# define HAVE_TMMINTRIN_H 1 +# define HAVE_SMMINTRIN_H 1 +# define HAVE_AVXINTRIN_H 1 +# if _MSC_VER >= 1600 +# define HAVE_WMMINTRIN_H 1 +# endif +# if _MSC_VER >= 1700 && defined(_M_X64) +# define HAVE_AVX2INTRIN_H 1 +# endif +# if _MSC_VER >= 1910 && defined(_M_X64) +# define HAVE_AVX512FINTRIN_H 1 +# endif + +# elif defined(_M_ARM64) + +# ifndef __ARM_ARCH +# define __ARM_ARCH 1 +# endif +# ifndef __ARM_NEON +# define __ARM_NEON 1 +# endif +# define HAVE_ARMCRYPTO 1 + +# endif /* _MSC_VER */ + +#elif defined(HAVE_INTRIN_H) +# include +#endif + +#ifdef HAVE_LIBCTGRIND +extern void ct_poison (const void *, size_t); +extern void ct_unpoison(const void *, size_t); +# define POISON(X, L) ct_poison((X), (L)) +# define UNPOISON(X, L) ct_unpoison((X), (L)) +#else +# define POISON(X, L) (void) 0 +# define UNPOISON(X, L) (void) 0 +#endif + +#ifdef HAVE_GCC_MEMORY_FENCES +# define ACQUIRE_FENCE __atomic_thread_fence(__ATOMIC_ACQUIRE) +#elif defined(HAVE_C11_MEMORY_FENCES) +# define ACQUIRE_FENCE atomic_thread_fence(memory_order_acquire) +#else +# define ACQUIRE_FENCE (void) 0 +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/crypto_hash_sha512.h b/packages/runtime/vendor/libsodium/crypto_hash_sha512.h new file mode 100644 index 0000000000..5b690fb25d --- /dev/null +++ b/packages/runtime/vendor/libsodium/crypto_hash_sha512.h @@ -0,0 +1,60 @@ +#ifndef crypto_hash_sha512_H +#define crypto_hash_sha512_H + +/* + * WARNING: Unless you absolutely need to use SHA512 for interoperability, + * purposes, you might want to consider crypto_generichash() instead. + * Unlike SHA512, crypto_generichash() is not vulnerable to length + * extension attacks. + */ + +#include +#include +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +typedef struct crypto_hash_sha512_state { + uint64_t state[8]; + uint64_t count[2]; + uint8_t buf[128]; +} crypto_hash_sha512_state; + +SODIUM_EXPORT +size_t crypto_hash_sha512_statebytes(void); + +#define crypto_hash_sha512_BYTES 64U +SODIUM_EXPORT +size_t crypto_hash_sha512_bytes(void); + +SODIUM_EXPORT +int crypto_hash_sha512(unsigned char *out, const unsigned char *in, + unsigned long long inlen) __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_hash_sha512_init(crypto_hash_sha512_state *state) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_hash_sha512_update(crypto_hash_sha512_state *state, + const unsigned char *in, + unsigned long long inlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_hash_sha512_final(crypto_hash_sha512_state *state, + unsigned char *out) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/crypto_scalarmult_curve25519.h b/packages/runtime/vendor/libsodium/crypto_scalarmult_curve25519.h new file mode 100644 index 0000000000..60e9d0c5a4 --- /dev/null +++ b/packages/runtime/vendor/libsodium/crypto_scalarmult_curve25519.h @@ -0,0 +1,42 @@ +#ifndef crypto_scalarmult_curve25519_H +#define crypto_scalarmult_curve25519_H + +#include + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_scalarmult_curve25519_BYTES 32U +SODIUM_EXPORT +size_t crypto_scalarmult_curve25519_bytes(void); + +#define crypto_scalarmult_curve25519_SCALARBYTES 32U +SODIUM_EXPORT +size_t crypto_scalarmult_curve25519_scalarbytes(void); + +/* + * NOTE: Do not use the result of this function directly for key exchange. + * + * Hash the result with the public keys in order to compute a shared + * secret key: H(q || client_pk || server_pk) + * + * Or unless this is not an option, use the crypto_kx() API instead. + */ +SODIUM_EXPORT +int crypto_scalarmult_curve25519(unsigned char *q, const unsigned char *n, + const unsigned char *p) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_scalarmult_curve25519_base(unsigned char *q, + const unsigned char *n) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/crypto_sign_ed25519.h b/packages/runtime/vendor/libsodium/crypto_sign_ed25519.h new file mode 100644 index 0000000000..0fdac42d35 --- /dev/null +++ b/packages/runtime/vendor/libsodium/crypto_sign_ed25519.h @@ -0,0 +1,124 @@ +#ifndef crypto_sign_ed25519_H +#define crypto_sign_ed25519_H + +#include +#include "crypto_hash_sha512.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +typedef struct crypto_sign_ed25519ph_state { + crypto_hash_sha512_state hs; +} crypto_sign_ed25519ph_state; + +SODIUM_EXPORT +size_t crypto_sign_ed25519ph_statebytes(void); + +#define crypto_sign_ed25519_BYTES 64U +SODIUM_EXPORT +size_t crypto_sign_ed25519_bytes(void); + +#define crypto_sign_ed25519_SEEDBYTES 32U +SODIUM_EXPORT +size_t crypto_sign_ed25519_seedbytes(void); + +#define crypto_sign_ed25519_PUBLICKEYBYTES 32U +SODIUM_EXPORT +size_t crypto_sign_ed25519_publickeybytes(void); + +#define crypto_sign_ed25519_SECRETKEYBYTES (32U + 32U) +SODIUM_EXPORT +size_t crypto_sign_ed25519_secretkeybytes(void); + +#define crypto_sign_ed25519_MESSAGEBYTES_MAX (SODIUM_SIZE_MAX - crypto_sign_ed25519_BYTES) +SODIUM_EXPORT +size_t crypto_sign_ed25519_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_sign_ed25519(unsigned char *sm, unsigned long long *smlen_p, + const unsigned char *m, unsigned long long mlen, + const unsigned char *sk) + __attribute__ ((nonnull(1, 5))); + +SODIUM_EXPORT +int crypto_sign_ed25519_open(unsigned char *m, unsigned long long *mlen_p, + const unsigned char *sm, unsigned long long smlen, + const unsigned char *pk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 5))); + +SODIUM_EXPORT +int crypto_sign_ed25519_detached(unsigned char *sig, + unsigned long long *siglen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *sk) + __attribute__ ((nonnull(1, 5))); + +SODIUM_EXPORT +int crypto_sign_ed25519_verify_detached(const unsigned char *sig, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *pk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +int crypto_sign_ed25519_keypair(unsigned char *pk, unsigned char *sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519_seed_keypair(unsigned char *pk, unsigned char *sk, + const unsigned char *seed) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519_pk_to_curve25519(unsigned char *curve25519_pk, + const unsigned char *ed25519_pk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519_sk_to_curve25519(unsigned char *curve25519_sk, + const unsigned char *ed25519_sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519_sk_to_seed(unsigned char *seed, + const unsigned char *sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519_sk_to_pk(unsigned char *pk, const unsigned char *sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519ph_init(crypto_sign_ed25519ph_state *state) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519ph_update(crypto_sign_ed25519ph_state *state, + const unsigned char *m, + unsigned long long mlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_sign_ed25519ph_final_create(crypto_sign_ed25519ph_state *state, + unsigned char *sig, + unsigned long long *siglen_p, + const unsigned char *sk) + __attribute__ ((nonnull(1, 2, 4))); + +SODIUM_EXPORT +int crypto_sign_ed25519ph_final_verify(crypto_sign_ed25519ph_state *state, + const unsigned char *sig, + const unsigned char *pk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/crypto_verify_16.h b/packages/runtime/vendor/libsodium/crypto_verify_16.h new file mode 100644 index 0000000000..7b9c8077ad --- /dev/null +++ b/packages/runtime/vendor/libsodium/crypto_verify_16.h @@ -0,0 +1,23 @@ +#ifndef crypto_verify_16_H +#define crypto_verify_16_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_verify_16_BYTES 16U +SODIUM_EXPORT +size_t crypto_verify_16_bytes(void); + +SODIUM_EXPORT +int crypto_verify_16(const unsigned char *x, const unsigned char *y) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/crypto_verify_32.h b/packages/runtime/vendor/libsodium/crypto_verify_32.h new file mode 100644 index 0000000000..9b0f4529f6 --- /dev/null +++ b/packages/runtime/vendor/libsodium/crypto_verify_32.h @@ -0,0 +1,23 @@ +#ifndef crypto_verify_32_H +#define crypto_verify_32_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_verify_32_BYTES 32U +SODIUM_EXPORT +size_t crypto_verify_32_bytes(void); + +SODIUM_EXPORT +int crypto_verify_32(const unsigned char *x, const unsigned char *y) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/crypto_verify_64.h b/packages/runtime/vendor/libsodium/crypto_verify_64.h new file mode 100644 index 0000000000..c83b73025a --- /dev/null +++ b/packages/runtime/vendor/libsodium/crypto_verify_64.h @@ -0,0 +1,23 @@ +#ifndef crypto_verify_64_H +#define crypto_verify_64_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_verify_64_BYTES 64U +SODIUM_EXPORT +size_t crypto_verify_64_bytes(void); + +SODIUM_EXPORT +int crypto_verify_64(const unsigned char *x, const unsigned char *y) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/ed25519_ref10.c b/packages/runtime/vendor/libsodium/ed25519_ref10.c new file mode 100644 index 0000000000..ac7a41492c --- /dev/null +++ b/packages/runtime/vendor/libsodium/ed25519_ref10.c @@ -0,0 +1,2858 @@ +#include +#include +#include +#include + +#include "crypto_verify_32.h" +#include "common.h" +#include "ed25519_ref10.h" +#include "utils.h" + +static inline uint64_t +load_3(const unsigned char *in) +{ + uint64_t result; + + result = (uint64_t) in[0]; + result |= ((uint64_t) in[1]) << 8; + result |= ((uint64_t) in[2]) << 16; + + return result; +} + +static inline uint64_t +load_4(const unsigned char *in) +{ + uint64_t result; + + result = (uint64_t) in[0]; + result |= ((uint64_t) in[1]) << 8; + result |= ((uint64_t) in[2]) << 16; + result |= ((uint64_t) in[3]) << 24; + + return result; +} + +/* + * Field arithmetic: + * Use 5*51 bit limbs on 64-bit systems with support for 128 bit arithmetic, + * and 10*25.5 bit limbs elsewhere. + * + * Functions used elsewhere that are candidates for inlining are defined + * via "private/curve25519_ref10.h". + */ + +#ifdef HAVE_TI_MODE +# include "fe_51/constants.h" +# include "fe_51/fe.h" +#else +# include "fe_25_5/constants.h" +# include "fe_25_5/fe.h" +#endif + +void +fe25519_invert(fe25519 out, const fe25519 z) +{ + fe25519 t0; + fe25519 t1; + fe25519 t2; + fe25519 t3; + int i; + + fe25519_sq(t0, z); + fe25519_sq(t1, t0); + fe25519_sq(t1, t1); + fe25519_mul(t1, z, t1); + fe25519_mul(t0, t0, t1); + fe25519_sq(t2, t0); + fe25519_mul(t1, t1, t2); + fe25519_sq(t2, t1); + for (i = 1; i < 5; ++i) { + fe25519_sq(t2, t2); + } + fe25519_mul(t1, t2, t1); + fe25519_sq(t2, t1); + for (i = 1; i < 10; ++i) { + fe25519_sq(t2, t2); + } + fe25519_mul(t2, t2, t1); + fe25519_sq(t3, t2); + for (i = 1; i < 20; ++i) { + fe25519_sq(t3, t3); + } + fe25519_mul(t2, t3, t2); + for (i = 1; i < 11; ++i) { + fe25519_sq(t2, t2); + } + fe25519_mul(t1, t2, t1); + fe25519_sq(t2, t1); + for (i = 1; i < 50; ++i) { + fe25519_sq(t2, t2); + } + fe25519_mul(t2, t2, t1); + fe25519_sq(t3, t2); + for (i = 1; i < 100; ++i) { + fe25519_sq(t3, t3); + } + fe25519_mul(t2, t3, t2); + for (i = 1; i < 51; ++i) { + fe25519_sq(t2, t2); + } + fe25519_mul(t1, t2, t1); + for (i = 1; i < 6; ++i) { + fe25519_sq(t1, t1); + } + fe25519_mul(out, t1, t0); +} + +static void +fe25519_pow22523(fe25519 out, const fe25519 z) +{ + fe25519 t0; + fe25519 t1; + fe25519 t2; + int i; + + fe25519_sq(t0, z); + fe25519_sq(t1, t0); + fe25519_sq(t1, t1); + fe25519_mul(t1, z, t1); + fe25519_mul(t0, t0, t1); + fe25519_sq(t0, t0); + fe25519_mul(t0, t1, t0); + fe25519_sq(t1, t0); + for (i = 1; i < 5; ++i) { + fe25519_sq(t1, t1); + } + fe25519_mul(t0, t1, t0); + fe25519_sq(t1, t0); + for (i = 1; i < 10; ++i) { + fe25519_sq(t1, t1); + } + fe25519_mul(t1, t1, t0); + fe25519_sq(t2, t1); + for (i = 1; i < 20; ++i) { + fe25519_sq(t2, t2); + } + fe25519_mul(t1, t2, t1); + for (i = 1; i < 11; ++i) { + fe25519_sq(t1, t1); + } + fe25519_mul(t0, t1, t0); + fe25519_sq(t1, t0); + for (i = 1; i < 50; ++i) { + fe25519_sq(t1, t1); + } + fe25519_mul(t1, t1, t0); + fe25519_sq(t2, t1); + for (i = 1; i < 100; ++i) { + fe25519_sq(t2, t2); + } + fe25519_mul(t1, t2, t1); + for (i = 1; i < 51; ++i) { + fe25519_sq(t1, t1); + } + fe25519_mul(t0, t1, t0); + fe25519_sq(t0, t0); + fe25519_sq(t0, t0); + fe25519_mul(out, t0, z); +} + +static inline void +fe25519_cneg(fe25519 h, const fe25519 f, unsigned int b) +{ + fe25519 negf; + + fe25519_neg(negf, f); + fe25519_copy(h, f); + fe25519_cmov(h, negf, b); +} + +static inline void +fe25519_abs(fe25519 h, const fe25519 f) +{ + fe25519_cneg(h, f, fe25519_isnegative(f)); +} + +static inline void +fe25519_sqmul(fe25519 s, const int n, const fe25519 a) +{ + int i; + + for (i = 0; i < n; i++) { + fe25519_sq(s, s); + } + fe25519_mul(s, s, a); +} + +static unsigned int +fe25519_notsquare(const fe25519 x) +{ + fe25519 _10, _11, _1100, _1111, _11110000, _11111111; + fe25519 t, u, v; + unsigned char s[32]; + + /* Jacobi symbol - x^((p-1)/2) */ + fe25519_mul(_10, x, x); + fe25519_mul(_11, x, _10); + fe25519_sq(_1100, _11); + fe25519_sq(_1100, _1100); + fe25519_mul(_1111, _11, _1100); + fe25519_sq(_11110000, _1111); + fe25519_sq(_11110000, _11110000); + fe25519_sq(_11110000, _11110000); + fe25519_sq(_11110000, _11110000); + fe25519_mul(_11111111, _1111, _11110000); + fe25519_copy(t, _11111111); + fe25519_sqmul(t, 2, _11); + fe25519_copy(u, t); + fe25519_sqmul(t, 10, u); + fe25519_sqmul(t, 10, u); + fe25519_copy(v, t); + fe25519_sqmul(t, 30, v); + fe25519_copy(v, t); + fe25519_sqmul(t, 60, v); + fe25519_copy(v, t); + fe25519_sqmul(t, 120, v); + fe25519_sqmul(t, 10, u); + fe25519_sqmul(t, 3, _11); + fe25519_sq(t, t); + + fe25519_tobytes(s, t); + + return s[1] & 1; +} + +/* + r = p + q + */ + +void +ge25519_add(ge25519_p1p1 *r, const ge25519_p3 *p, const ge25519_cached *q) +{ + fe25519 t0; + + fe25519_add(r->X, p->Y, p->X); + fe25519_sub(r->Y, p->Y, p->X); + fe25519_mul(r->Z, r->X, q->YplusX); + fe25519_mul(r->Y, r->Y, q->YminusX); + fe25519_mul(r->T, q->T2d, p->T); + fe25519_mul(r->X, p->Z, q->Z); + fe25519_add(t0, r->X, r->X); + fe25519_sub(r->X, r->Z, r->Y); + fe25519_add(r->Y, r->Z, r->Y); + fe25519_add(r->Z, t0, r->T); + fe25519_sub(r->T, t0, r->T); +} + +static void +slide_vartime(signed char *r, const unsigned char *a) +{ + int i; + int b; + int k; + int ribs; + int cmp; + + for (i = 0; i < 256; ++i) { + r[i] = 1 & (a[i >> 3] >> (i & 7)); + } + for (i = 0; i < 256; ++i) { + if (! r[i]) { + continue; + } + for (b = 1; b <= 6 && i + b < 256; ++b) { + if (! r[i + b]) { + continue; + } + ribs = r[i + b] << b; + cmp = r[i] + ribs; + if (cmp <= 15) { + r[i] = cmp; + r[i + b] = 0; + } else { + cmp = r[i] - ribs; + if (cmp < -15) { + break; + } + r[i] = cmp; + for (k = i + b; k < 256; ++k) { + if (! r[k]) { + r[k] = 1; + break; + } + r[k] = 0; + } + } + } + } +} + +int +ge25519_frombytes(ge25519_p3 *h, const unsigned char *s) +{ + fe25519 u; + fe25519 v; + fe25519 vxx; + fe25519 m_root_check, p_root_check; + fe25519 negx; + fe25519 x_sqrtm1; + int has_m_root, has_p_root; + + fe25519_frombytes(h->Y, s); + fe25519_1(h->Z); + fe25519_sq(u, h->Y); + fe25519_mul(v, u, d); + fe25519_sub(u, u, h->Z); /* u = y^2-1 */ + fe25519_add(v, v, h->Z); /* v = dy^2+1 */ + + fe25519_mul(h->X, u, v); + fe25519_pow22523(h->X, h->X); + fe25519_mul(h->X, u, h->X); /* u((uv)^((q-5)/8)) */ + + fe25519_sq(vxx, h->X); + fe25519_mul(vxx, vxx, v); + fe25519_sub(m_root_check, vxx, u); /* vx^2-u */ + fe25519_add(p_root_check, vxx, u); /* vx^2+u */ + has_m_root = fe25519_iszero(m_root_check); + has_p_root = fe25519_iszero(p_root_check); + fe25519_mul(x_sqrtm1, h->X, sqrtm1); /* x*sqrt(-1) */ + fe25519_cmov(h->X, x_sqrtm1, 1 - has_m_root); + + fe25519_neg(negx, h->X); + fe25519_cmov(h->X, negx, fe25519_isnegative(h->X) ^ (s[31] >> 7)); + fe25519_mul(h->T, h->X, h->Y); + + return (has_m_root | has_p_root) - 1; +} + +int +ge25519_frombytes_negate_vartime(ge25519_p3 *h, const unsigned char *s) +{ + fe25519 u; + fe25519 v; + fe25519 v3; + fe25519 vxx; + fe25519 m_root_check, p_root_check; + + fe25519_frombytes(h->Y, s); + fe25519_1(h->Z); + fe25519_sq(u, h->Y); + fe25519_mul(v, u, d); + fe25519_sub(u, u, h->Z); /* u = y^2-1 */ + fe25519_add(v, v, h->Z); /* v = dy^2+1 */ + + fe25519_sq(v3, v); + fe25519_mul(v3, v3, v); /* v3 = v^3 */ + fe25519_sq(h->X, v3); + fe25519_mul(h->X, h->X, v); + fe25519_mul(h->X, h->X, u); /* x = uv^7 */ + + fe25519_pow22523(h->X, h->X); /* x = (uv^7)^((q-5)/8) */ + fe25519_mul(h->X, h->X, v3); + fe25519_mul(h->X, h->X, u); /* x = uv^3(uv^7)^((q-5)/8) */ + + fe25519_sq(vxx, h->X); + fe25519_mul(vxx, vxx, v); + fe25519_sub(m_root_check, vxx, u); /* vx^2-u */ + if (fe25519_iszero(m_root_check) == 0) { + fe25519_add(p_root_check, vxx, u); /* vx^2+u */ + if (fe25519_iszero(p_root_check) == 0) { + return -1; + } + fe25519_mul(h->X, h->X, sqrtm1); + } + + if (fe25519_isnegative(h->X) == (s[31] >> 7)) { + fe25519_neg(h->X, h->X); + } + fe25519_mul(h->T, h->X, h->Y); + + return 0; +} + +/* + r = p + q + */ + +static void +ge25519_madd(ge25519_p1p1 *r, const ge25519_p3 *p, const ge25519_precomp *q) +{ + fe25519 t0; + + fe25519_add(r->X, p->Y, p->X); + fe25519_sub(r->Y, p->Y, p->X); + fe25519_mul(r->Z, r->X, q->yplusx); + fe25519_mul(r->Y, r->Y, q->yminusx); + fe25519_mul(r->T, q->xy2d, p->T); + fe25519_add(t0, p->Z, p->Z); + fe25519_sub(r->X, r->Z, r->Y); + fe25519_add(r->Y, r->Z, r->Y); + fe25519_add(r->Z, t0, r->T); + fe25519_sub(r->T, t0, r->T); +} + +/* + r = p - q + */ + +static void +ge25519_msub(ge25519_p1p1 *r, const ge25519_p3 *p, const ge25519_precomp *q) +{ + fe25519 t0; + + fe25519_add(r->X, p->Y, p->X); + fe25519_sub(r->Y, p->Y, p->X); + fe25519_mul(r->Z, r->X, q->yminusx); + fe25519_mul(r->Y, r->Y, q->yplusx); + fe25519_mul(r->T, q->xy2d, p->T); + fe25519_add(t0, p->Z, p->Z); + fe25519_sub(r->X, r->Z, r->Y); + fe25519_add(r->Y, r->Z, r->Y); + fe25519_sub(r->Z, t0, r->T); + fe25519_add(r->T, t0, r->T); +} + +/* + r = p + */ + +void +ge25519_p1p1_to_p2(ge25519_p2 *r, const ge25519_p1p1 *p) +{ + fe25519_mul(r->X, p->X, p->T); + fe25519_mul(r->Y, p->Y, p->Z); + fe25519_mul(r->Z, p->Z, p->T); +} + +/* + r = p + */ + +void +ge25519_p1p1_to_p3(ge25519_p3 *r, const ge25519_p1p1 *p) +{ + fe25519_mul(r->X, p->X, p->T); + fe25519_mul(r->Y, p->Y, p->Z); + fe25519_mul(r->Z, p->Z, p->T); + fe25519_mul(r->T, p->X, p->Y); +} + +static void +ge25519_p2_0(ge25519_p2 *h) +{ + fe25519_0(h->X); + fe25519_1(h->Y); + fe25519_1(h->Z); +} + +/* + r = 2 * p + */ + +static void +ge25519_p2_dbl(ge25519_p1p1 *r, const ge25519_p2 *p) +{ + fe25519 t0; + + fe25519_sq(r->X, p->X); + fe25519_sq(r->Z, p->Y); + fe25519_sq2(r->T, p->Z); + fe25519_add(r->Y, p->X, p->Y); + fe25519_sq(t0, r->Y); + fe25519_add(r->Y, r->Z, r->X); + fe25519_sub(r->Z, r->Z, r->X); + fe25519_sub(r->X, t0, r->Y); + fe25519_sub(r->T, r->T, r->Z); +} + +static void +ge25519_p3_0(ge25519_p3 *h) +{ + fe25519_0(h->X); + fe25519_1(h->Y); + fe25519_1(h->Z); + fe25519_0(h->T); +} + +static void +ge25519_cached_0(ge25519_cached *h) +{ + fe25519_1(h->YplusX); + fe25519_1(h->YminusX); + fe25519_1(h->Z); + fe25519_0(h->T2d); +} + +/* + r = p + */ + +void +ge25519_p3_to_cached(ge25519_cached *r, const ge25519_p3 *p) +{ + fe25519_add(r->YplusX, p->Y, p->X); + fe25519_sub(r->YminusX, p->Y, p->X); + fe25519_copy(r->Z, p->Z); + fe25519_mul(r->T2d, p->T, d2); +} + +static void +ge25519_p3_to_precomp(ge25519_precomp *pi, const ge25519_p3 *p) +{ + fe25519 recip; + fe25519 x; + fe25519 y; + fe25519 xy; + + fe25519_invert(recip, p->Z); + fe25519_mul(x, p->X, recip); + fe25519_mul(y, p->Y, recip); + fe25519_add(pi->yplusx, y, x); + fe25519_sub(pi->yminusx, y, x); + fe25519_mul(xy, x, y); + fe25519_mul(pi->xy2d, xy, d2); +} + +/* + r = p + */ + +static void +ge25519_p3_to_p2(ge25519_p2 *r, const ge25519_p3 *p) +{ + fe25519_copy(r->X, p->X); + fe25519_copy(r->Y, p->Y); + fe25519_copy(r->Z, p->Z); +} + +void +ge25519_p3_tobytes(unsigned char *s, const ge25519_p3 *h) +{ + fe25519 recip; + fe25519 x; + fe25519 y; + + fe25519_invert(recip, h->Z); + fe25519_mul(x, h->X, recip); + fe25519_mul(y, h->Y, recip); + fe25519_tobytes(s, y); + s[31] ^= fe25519_isnegative(x) << 7; +} + +/* + r = 2 * p + */ + +static void +ge25519_p3_dbl(ge25519_p1p1 *r, const ge25519_p3 *p) +{ + ge25519_p2 q; + ge25519_p3_to_p2(&q, p); + ge25519_p2_dbl(r, &q); +} + +static void +ge25519_precomp_0(ge25519_precomp *h) +{ + fe25519_1(h->yplusx); + fe25519_1(h->yminusx); + fe25519_0(h->xy2d); +} + +static unsigned char +equal(signed char b, signed char c) +{ + unsigned char ub = b; + unsigned char uc = c; + unsigned char x = ub ^ uc; /* 0: yes; 1..255: no */ + uint32_t y = (uint32_t) x; /* 0: yes; 1..255: no */ + + y -= 1; /* 4294967295: yes; 0..254: no */ + y >>= 31; /* 1: yes; 0: no */ + + return y; +} + +static unsigned char +negative(signed char b) +{ + /* 18446744073709551361..18446744073709551615: yes; 0..255: no */ + uint64_t x = b; + + x >>= 63; /* 1: yes; 0: no */ + + return x; +} + +static void +ge25519_cmov(ge25519_precomp *t, const ge25519_precomp *u, unsigned char b) +{ + fe25519_cmov(t->yplusx, u->yplusx, b); + fe25519_cmov(t->yminusx, u->yminusx, b); + fe25519_cmov(t->xy2d, u->xy2d, b); +} + +static void +ge25519_cmov_cached(ge25519_cached *t, const ge25519_cached *u, unsigned char b) +{ + fe25519_cmov(t->YplusX, u->YplusX, b); + fe25519_cmov(t->YminusX, u->YminusX, b); + fe25519_cmov(t->Z, u->Z, b); + fe25519_cmov(t->T2d, u->T2d, b); +} + +static void +ge25519_cmov8(ge25519_precomp *t, const ge25519_precomp precomp[8], const signed char b) +{ + ge25519_precomp minust; + const unsigned char bnegative = negative(b); + const unsigned char babs = b - (((-bnegative) & b) * ((signed char) 1 << 1)); + + ge25519_precomp_0(t); + ge25519_cmov(t, &precomp[0], equal(babs, 1)); + ge25519_cmov(t, &precomp[1], equal(babs, 2)); + ge25519_cmov(t, &precomp[2], equal(babs, 3)); + ge25519_cmov(t, &precomp[3], equal(babs, 4)); + ge25519_cmov(t, &precomp[4], equal(babs, 5)); + ge25519_cmov(t, &precomp[5], equal(babs, 6)); + ge25519_cmov(t, &precomp[6], equal(babs, 7)); + ge25519_cmov(t, &precomp[7], equal(babs, 8)); + fe25519_copy(minust.yplusx, t->yminusx); + fe25519_copy(minust.yminusx, t->yplusx); + fe25519_neg(minust.xy2d, t->xy2d); + ge25519_cmov(t, &minust, bnegative); +} + +static void +ge25519_cmov8_base(ge25519_precomp *t, const int pos, const signed char b) +{ + static const ge25519_precomp base[32][8] = { /* base[i][j] = (j+1)*256^i*B */ +#ifdef HAVE_TI_MODE +# include "fe_51/base.h" +#else +# include "fe_25_5/base.h" +#endif + }; + ge25519_cmov8(t, base[pos], b); +} + +static void +ge25519_cmov8_cached(ge25519_cached *t, const ge25519_cached cached[8], const signed char b) +{ + ge25519_cached minust; + const unsigned char bnegative = negative(b); + const unsigned char babs = b - (((-bnegative) & b) * ((signed char) 1 << 1)); + + ge25519_cached_0(t); + ge25519_cmov_cached(t, &cached[0], equal(babs, 1)); + ge25519_cmov_cached(t, &cached[1], equal(babs, 2)); + ge25519_cmov_cached(t, &cached[2], equal(babs, 3)); + ge25519_cmov_cached(t, &cached[3], equal(babs, 4)); + ge25519_cmov_cached(t, &cached[4], equal(babs, 5)); + ge25519_cmov_cached(t, &cached[5], equal(babs, 6)); + ge25519_cmov_cached(t, &cached[6], equal(babs, 7)); + ge25519_cmov_cached(t, &cached[7], equal(babs, 8)); + fe25519_copy(minust.YplusX, t->YminusX); + fe25519_copy(minust.YminusX, t->YplusX); + fe25519_copy(minust.Z, t->Z); + fe25519_neg(minust.T2d, t->T2d); + ge25519_cmov_cached(t, &minust, bnegative); +} + +/* + r = p - q + */ + +void +ge25519_sub(ge25519_p1p1 *r, const ge25519_p3 *p, const ge25519_cached *q) +{ + fe25519 t0; + + fe25519_add(r->X, p->Y, p->X); + fe25519_sub(r->Y, p->Y, p->X); + fe25519_mul(r->Z, r->X, q->YminusX); + fe25519_mul(r->Y, r->Y, q->YplusX); + fe25519_mul(r->T, q->T2d, p->T); + fe25519_mul(r->X, p->Z, q->Z); + fe25519_add(t0, r->X, r->X); + fe25519_sub(r->X, r->Z, r->Y); + fe25519_add(r->Y, r->Z, r->Y); + fe25519_sub(r->Z, t0, r->T); + fe25519_add(r->T, t0, r->T); +} + +void +ge25519_tobytes(unsigned char *s, const ge25519_p2 *h) +{ + fe25519 recip; + fe25519 x; + fe25519 y; + + fe25519_invert(recip, h->Z); + fe25519_mul(x, h->X, recip); + fe25519_mul(y, h->Y, recip); + fe25519_tobytes(s, y); + s[31] ^= fe25519_isnegative(x) << 7; +} + +/* + r = a * A + b * B + where a = a[0]+256*a[1]+...+256^31 a[31]. + and b = b[0]+256*b[1]+...+256^31 b[31]. + B is the Ed25519 base point (x,4/5) with x positive. + + Only used for signatures verification. + */ + +void +ge25519_double_scalarmult_vartime(ge25519_p2 *r, const unsigned char *a, + const ge25519_p3 *A, const unsigned char *b) +{ + static const ge25519_precomp Bi[8] = { +#ifdef HAVE_TI_MODE +# include "fe_51/base2.h" +#else +# include "fe_25_5/base2.h" +#endif + }; + signed char aslide[256]; + signed char bslide[256]; + ge25519_cached Ai[8]; /* A,3A,5A,7A,9A,11A,13A,15A */ + ge25519_p1p1 t; + ge25519_p3 u; + ge25519_p3 A2; + int i; + + slide_vartime(aslide, a); + slide_vartime(bslide, b); + + ge25519_p3_to_cached(&Ai[0], A); + + ge25519_p3_dbl(&t, A); + ge25519_p1p1_to_p3(&A2, &t); + + ge25519_add(&t, &A2, &Ai[0]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[1], &u); + + ge25519_add(&t, &A2, &Ai[1]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[2], &u); + + ge25519_add(&t, &A2, &Ai[2]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[3], &u); + + ge25519_add(&t, &A2, &Ai[3]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[4], &u); + + ge25519_add(&t, &A2, &Ai[4]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[5], &u); + + ge25519_add(&t, &A2, &Ai[5]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[6], &u); + + ge25519_add(&t, &A2, &Ai[6]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[7], &u); + + ge25519_p2_0(r); + + for (i = 255; i >= 0; --i) { + if (aslide[i] || bslide[i]) { + break; + } + } + + for (; i >= 0; --i) { + ge25519_p2_dbl(&t, r); + + if (aslide[i] > 0) { + ge25519_p1p1_to_p3(&u, &t); + ge25519_add(&t, &u, &Ai[aslide[i] / 2]); + } else if (aslide[i] < 0) { + ge25519_p1p1_to_p3(&u, &t); + ge25519_sub(&t, &u, &Ai[(-aslide[i]) / 2]); + } + + if (bslide[i] > 0) { + ge25519_p1p1_to_p3(&u, &t); + ge25519_madd(&t, &u, &Bi[bslide[i] / 2]); + } else if (bslide[i] < 0) { + ge25519_p1p1_to_p3(&u, &t); + ge25519_msub(&t, &u, &Bi[(-bslide[i]) / 2]); + } + + ge25519_p1p1_to_p2(r, &t); + } +} + +/* + h = a * p + where a = a[0]+256*a[1]+...+256^31 a[31] + + Preconditions: + a[31] <= 127 + + p is public + */ + +void +ge25519_scalarmult(ge25519_p3 *h, const unsigned char *a, const ge25519_p3 *p) +{ + signed char e[64]; + signed char carry; + ge25519_p1p1 r; + ge25519_p2 s; + ge25519_p1p1 t2, t3, t4, t5, t6, t7, t8; + ge25519_p3 p2, p3, p4, p5, p6, p7, p8; + ge25519_cached pi[8]; + ge25519_cached t; + int i; + + ge25519_p3_to_cached(&pi[1 - 1], p); /* p */ + + ge25519_p3_dbl(&t2, p); + ge25519_p1p1_to_p3(&p2, &t2); + ge25519_p3_to_cached(&pi[2 - 1], &p2); /* 2p = 2*p */ + + ge25519_add(&t3, p, &pi[2 - 1]); + ge25519_p1p1_to_p3(&p3, &t3); + ge25519_p3_to_cached(&pi[3 - 1], &p3); /* 3p = 2p+p */ + + ge25519_p3_dbl(&t4, &p2); + ge25519_p1p1_to_p3(&p4, &t4); + ge25519_p3_to_cached(&pi[4 - 1], &p4); /* 4p = 2*2p */ + + ge25519_add(&t5, p, &pi[4 - 1]); + ge25519_p1p1_to_p3(&p5, &t5); + ge25519_p3_to_cached(&pi[5 - 1], &p5); /* 5p = 4p+p */ + + ge25519_p3_dbl(&t6, &p3); + ge25519_p1p1_to_p3(&p6, &t6); + ge25519_p3_to_cached(&pi[6 - 1], &p6); /* 6p = 2*3p */ + + ge25519_add(&t7, p, &pi[6 - 1]); + ge25519_p1p1_to_p3(&p7, &t7); + ge25519_p3_to_cached(&pi[7 - 1], &p7); /* 7p = 6p+p */ + + ge25519_p3_dbl(&t8, &p4); + ge25519_p1p1_to_p3(&p8, &t8); + ge25519_p3_to_cached(&pi[8 - 1], &p8); /* 8p = 2*4p */ + + for (i = 0; i < 32; ++i) { + e[2 * i + 0] = (a[i] >> 0) & 15; + e[2 * i + 1] = (a[i] >> 4) & 15; + } + /* each e[i] is between 0 and 15 */ + /* e[63] is between 0 and 7 */ + + carry = 0; + for (i = 0; i < 63; ++i) { + e[i] += carry; + carry = e[i] + 8; + carry >>= 4; + e[i] -= carry * ((signed char) 1 << 4); + } + e[63] += carry; + /* each e[i] is between -8 and 8 */ + + ge25519_p3_0(h); + + for (i = 63; i != 0; i--) { + ge25519_cmov8_cached(&t, pi, e[i]); + ge25519_add(&r, h, &t); + + ge25519_p1p1_to_p2(&s, &r); + ge25519_p2_dbl(&r, &s); + ge25519_p1p1_to_p2(&s, &r); + ge25519_p2_dbl(&r, &s); + ge25519_p1p1_to_p2(&s, &r); + ge25519_p2_dbl(&r, &s); + ge25519_p1p1_to_p2(&s, &r); + ge25519_p2_dbl(&r, &s); + + ge25519_p1p1_to_p3(h, &r); /* *16 */ + } + ge25519_cmov8_cached(&t, pi, e[i]); + ge25519_add(&r, h, &t); + + ge25519_p1p1_to_p3(h, &r); +} + +/* + h = a * B (with precomputation) + where a = a[0]+256*a[1]+...+256^31 a[31] + B is the Ed25519 base point (x,4/5) with x positive + (as bytes: 0x5866666666666666666666666666666666666666666666666666666666666666) + + Preconditions: + a[31] <= 127 + */ + +void +ge25519_scalarmult_base(ge25519_p3 *h, const unsigned char *a) +{ + signed char e[64]; + signed char carry; + ge25519_p1p1 r; + ge25519_p2 s; + ge25519_precomp t; + int i; + + for (i = 0; i < 32; ++i) { + e[2 * i + 0] = (a[i] >> 0) & 15; + e[2 * i + 1] = (a[i] >> 4) & 15; + } + /* each e[i] is between 0 and 15 */ + /* e[63] is between 0 and 7 */ + + carry = 0; + for (i = 0; i < 63; ++i) { + e[i] += carry; + carry = e[i] + 8; + carry >>= 4; + e[i] -= carry * ((signed char) 1 << 4); + } + e[63] += carry; + /* each e[i] is between -8 and 8 */ + + ge25519_p3_0(h); + + for (i = 1; i < 64; i += 2) { + ge25519_cmov8_base(&t, i / 2, e[i]); + ge25519_madd(&r, h, &t); + ge25519_p1p1_to_p3(h, &r); + } + + ge25519_p3_dbl(&r, h); + ge25519_p1p1_to_p2(&s, &r); + ge25519_p2_dbl(&r, &s); + ge25519_p1p1_to_p2(&s, &r); + ge25519_p2_dbl(&r, &s); + ge25519_p1p1_to_p2(&s, &r); + ge25519_p2_dbl(&r, &s); + ge25519_p1p1_to_p3(h, &r); + + for (i = 0; i < 64; i += 2) { + ge25519_cmov8_base(&t, i / 2, e[i]); + ge25519_madd(&r, h, &t); + ge25519_p1p1_to_p3(h, &r); + } +} + +/* multiply by the order of the main subgroup l = 2^252+27742317777372353535851937790883648493 */ +static void +ge25519_mul_l(ge25519_p3 *r, const ge25519_p3 *A) +{ + static const signed char aslide[253] = { + 13, 0, 0, 0, 0, -1, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, -13, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, -13, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 3, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 7, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 + }; + ge25519_cached Ai[8]; + ge25519_p1p1 t; + ge25519_p3 u; + ge25519_p3 A2; + int i; + + ge25519_p3_to_cached(&Ai[0], A); + ge25519_p3_dbl(&t, A); + ge25519_p1p1_to_p3(&A2, &t); + ge25519_add(&t, &A2, &Ai[0]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[1], &u); + ge25519_add(&t, &A2, &Ai[1]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[2], &u); + ge25519_add(&t, &A2, &Ai[2]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[3], &u); + ge25519_add(&t, &A2, &Ai[3]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[4], &u); + ge25519_add(&t, &A2, &Ai[4]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[5], &u); + ge25519_add(&t, &A2, &Ai[5]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[6], &u); + ge25519_add(&t, &A2, &Ai[6]); + ge25519_p1p1_to_p3(&u, &t); + ge25519_p3_to_cached(&Ai[7], &u); + + ge25519_p3_0(r); + + for (i = 252; i >= 0; --i) { + ge25519_p3_dbl(&t, r); + + if (aslide[i] > 0) { + ge25519_p1p1_to_p3(&u, &t); + ge25519_add(&t, &u, &Ai[aslide[i] / 2]); + } else if (aslide[i] < 0) { + ge25519_p1p1_to_p3(&u, &t); + ge25519_sub(&t, &u, &Ai[(-aslide[i]) / 2]); + } + + ge25519_p1p1_to_p3(r, &t); + } +} + +int +ge25519_is_on_curve(const ge25519_p3 *p) +{ + fe25519 x2; + fe25519 y2; + fe25519 z2; + fe25519 z4; + fe25519 t0; + fe25519 t1; + + fe25519_sq(x2, p->X); + fe25519_sq(y2, p->Y); + fe25519_sq(z2, p->Z); + fe25519_sub(t0, y2, x2); + fe25519_mul(t0, t0, z2); + + fe25519_mul(t1, x2, y2); + fe25519_mul(t1, t1, d); + fe25519_sq(z4, z2); + fe25519_add(t1, t1, z4); + fe25519_sub(t0, t0, t1); + + return fe25519_iszero(t0); +} + +int +ge25519_is_on_main_subgroup(const ge25519_p3 *p) +{ + ge25519_p3 pl; + + ge25519_mul_l(&pl, p); + + return fe25519_iszero(pl.X); +} + +int +ge25519_is_canonical(const unsigned char *s) +{ + unsigned char c; + unsigned char d; + unsigned int i; + + c = (s[31] & 0x7f) ^ 0x7f; + for (i = 30; i > 0; i--) { + c |= s[i] ^ 0xff; + } + c = (((unsigned int) c) - 1U) >> 8; + d = (0xed - 1U - (unsigned int) s[0]) >> 8; + + return 1 - (c & d & 1); +} + +int +ge25519_has_small_order(const unsigned char s[32]) +{ + CRYPTO_ALIGN(16) + static const unsigned char blacklist[][32] = { + /* 0 (order 4) */ + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + /* 1 (order 1) */ + { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, + /* 2707385501144840649318225287225658788936804267575313519463743609750303402022 + (order 8) */ + { 0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, + 0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, + 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x05 }, + /* 55188659117513257062467267217118295137698188065244968500265048394206261417927 + (order 8) */ + { 0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, + 0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, + 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0x7a }, + /* p-1 (order 2) */ + { 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f }, + /* p (=0, order 4) */ + { 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f }, + /* p+1 (=1, order 1) */ + { 0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f } + }; + unsigned char c[7] = { 0 }; + unsigned int k; + size_t i, j; + + COMPILER_ASSERT(7 == sizeof blacklist / sizeof blacklist[0]); + for (j = 0; j < 31; j++) { + for (i = 0; i < sizeof blacklist / sizeof blacklist[0]; i++) { + c[i] |= s[j] ^ blacklist[i][j]; + } + } + for (i = 0; i < sizeof blacklist / sizeof blacklist[0]; i++) { + c[i] |= (s[j] & 0x7f) ^ blacklist[i][j]; + } + k = 0; + for (i = 0; i < sizeof blacklist / sizeof blacklist[0]; i++) { + k |= (c[i] - 1); + } + return (int) ((k >> 8) & 1); +} + +/* + Input: + a[0]+256*a[1]+...+256^31*a[31] = a + b[0]+256*b[1]+...+256^31*b[31] = b + * + Output: + s[0]+256*s[1]+...+256^31*s[31] = (ab) mod l + where l = 2^252 + 27742317777372353535851937790883648493. + */ + +void +sc25519_mul(unsigned char s[32], const unsigned char a[32], const unsigned char b[32]) +{ + int64_t a0 = 2097151 & load_3(a); + int64_t a1 = 2097151 & (load_4(a + 2) >> 5); + int64_t a2 = 2097151 & (load_3(a + 5) >> 2); + int64_t a3 = 2097151 & (load_4(a + 7) >> 7); + int64_t a4 = 2097151 & (load_4(a + 10) >> 4); + int64_t a5 = 2097151 & (load_3(a + 13) >> 1); + int64_t a6 = 2097151 & (load_4(a + 15) >> 6); + int64_t a7 = 2097151 & (load_3(a + 18) >> 3); + int64_t a8 = 2097151 & load_3(a + 21); + int64_t a9 = 2097151 & (load_4(a + 23) >> 5); + int64_t a10 = 2097151 & (load_3(a + 26) >> 2); + int64_t a11 = (load_4(a + 28) >> 7); + + int64_t b0 = 2097151 & load_3(b); + int64_t b1 = 2097151 & (load_4(b + 2) >> 5); + int64_t b2 = 2097151 & (load_3(b + 5) >> 2); + int64_t b3 = 2097151 & (load_4(b + 7) >> 7); + int64_t b4 = 2097151 & (load_4(b + 10) >> 4); + int64_t b5 = 2097151 & (load_3(b + 13) >> 1); + int64_t b6 = 2097151 & (load_4(b + 15) >> 6); + int64_t b7 = 2097151 & (load_3(b + 18) >> 3); + int64_t b8 = 2097151 & load_3(b + 21); + int64_t b9 = 2097151 & (load_4(b + 23) >> 5); + int64_t b10 = 2097151 & (load_3(b + 26) >> 2); + int64_t b11 = (load_4(b + 28) >> 7); + + int64_t s0; + int64_t s1; + int64_t s2; + int64_t s3; + int64_t s4; + int64_t s5; + int64_t s6; + int64_t s7; + int64_t s8; + int64_t s9; + int64_t s10; + int64_t s11; + int64_t s12; + int64_t s13; + int64_t s14; + int64_t s15; + int64_t s16; + int64_t s17; + int64_t s18; + int64_t s19; + int64_t s20; + int64_t s21; + int64_t s22; + int64_t s23; + + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + int64_t carry10; + int64_t carry11; + int64_t carry12; + int64_t carry13; + int64_t carry14; + int64_t carry15; + int64_t carry16; + int64_t carry17; + int64_t carry18; + int64_t carry19; + int64_t carry20; + int64_t carry21; + int64_t carry22; + + s0 = a0 * b0; + s1 = a0 * b1 + a1 * b0; + s2 = a0 * b2 + a1 * b1 + a2 * b0; + s3 = a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0; + s4 = a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0; + s5 = a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0; + s6 = a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0; + s7 = a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + + a6 * b1 + a7 * b0; + s8 = a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + + a6 * b2 + a7 * b1 + a8 * b0; + s9 = a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + + a6 * b3 + a7 * b2 + a8 * b1 + a9 * b0; + s10 = a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + + a6 * b4 + a7 * b3 + a8 * b2 + a9 * b1 + a10 * b0; + s11 = a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + + a6 * b5 + a7 * b4 + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0; + s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + + a7 * b5 + a8 * b4 + a9 * b3 + a10 * b2 + a11 * b1; + s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + + a8 * b5 + a9 * b4 + a10 * b3 + a11 * b2; + s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + + a9 * b5 + a10 * b4 + a11 * b3; + s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + + a10 * b5 + a11 * b4; + s16 = + a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5; + s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6; + s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7; + s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8; + s20 = a9 * b11 + a10 * b10 + a11 * b9; + s21 = a10 * b11 + a11 * b10; + s22 = a11 * b11; + s23 = 0; + + carry0 = (s0 + (int64_t) (1L << 20)) >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry2 = (s2 + (int64_t) (1L << 20)) >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry4 = (s4 + (int64_t) (1L << 20)) >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry12 = (s12 + (int64_t) (1L << 20)) >> 21; + s13 += carry12; + s12 -= carry12 * ((uint64_t) 1L << 21); + carry14 = (s14 + (int64_t) (1L << 20)) >> 21; + s15 += carry14; + s14 -= carry14 * ((uint64_t) 1L << 21); + carry16 = (s16 + (int64_t) (1L << 20)) >> 21; + s17 += carry16; + s16 -= carry16 * ((uint64_t) 1L << 21); + carry18 = (s18 + (int64_t) (1L << 20)) >> 21; + s19 += carry18; + s18 -= carry18 * ((uint64_t) 1L << 21); + carry20 = (s20 + (int64_t) (1L << 20)) >> 21; + s21 += carry20; + s20 -= carry20 * ((uint64_t) 1L << 21); + carry22 = (s22 + (int64_t) (1L << 20)) >> 21; + s23 += carry22; + s22 -= carry22 * ((uint64_t) 1L << 21); + + carry1 = (s1 + (int64_t) (1L << 20)) >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry3 = (s3 + (int64_t) (1L << 20)) >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry5 = (s5 + (int64_t) (1L << 20)) >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + carry13 = (s13 + (int64_t) (1L << 20)) >> 21; + s14 += carry13; + s13 -= carry13 * ((uint64_t) 1L << 21); + carry15 = (s15 + (int64_t) (1L << 20)) >> 21; + s16 += carry15; + s15 -= carry15 * ((uint64_t) 1L << 21); + carry17 = (s17 + (int64_t) (1L << 20)) >> 21; + s18 += carry17; + s17 -= carry17 * ((uint64_t) 1L << 21); + carry19 = (s19 + (int64_t) (1L << 20)) >> 21; + s20 += carry19; + s19 -= carry19 * ((uint64_t) 1L << 21); + carry21 = (s21 + (int64_t) (1L << 20)) >> 21; + s22 += carry21; + s21 -= carry21 * ((uint64_t) 1L << 21); + + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry12 = (s12 + (int64_t) (1L << 20)) >> 21; + s13 += carry12; + s12 -= carry12 * ((uint64_t) 1L << 21); + carry14 = (s14 + (int64_t) (1L << 20)) >> 21; + s15 += carry14; + s14 -= carry14 * ((uint64_t) 1L << 21); + carry16 = (s16 + (int64_t) (1L << 20)) >> 21; + s17 += carry16; + s16 -= carry16 * ((uint64_t) 1L << 21); + + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + carry13 = (s13 + (int64_t) (1L << 20)) >> 21; + s14 += carry13; + s13 -= carry13 * ((uint64_t) 1L << 21); + carry15 = (s15 + (int64_t) (1L << 20)) >> 21; + s16 += carry15; + s15 -= carry15 * ((uint64_t) 1L << 21); + + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = (s0 + (int64_t) (1L << 20)) >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry2 = (s2 + (int64_t) (1L << 20)) >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry4 = (s4 + (int64_t) (1L << 20)) >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + + carry1 = (s1 + (int64_t) (1L << 20)) >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry3 = (s3 + (int64_t) (1L << 20)) >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry5 = (s5 + (int64_t) (1L << 20)) >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry11 = s11 >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + + s[0] = s0 >> 0; + s[1] = s0 >> 8; + s[2] = (s0 >> 16) | (s1 * ((uint64_t) 1 << 5)); + s[3] = s1 >> 3; + s[4] = s1 >> 11; + s[5] = (s1 >> 19) | (s2 * ((uint64_t) 1 << 2)); + s[6] = s2 >> 6; + s[7] = (s2 >> 14) | (s3 * ((uint64_t) 1 << 7)); + s[8] = s3 >> 1; + s[9] = s3 >> 9; + s[10] = (s3 >> 17) | (s4 * ((uint64_t) 1 << 4)); + s[11] = s4 >> 4; + s[12] = s4 >> 12; + s[13] = (s4 >> 20) | (s5 * ((uint64_t) 1 << 1)); + s[14] = s5 >> 7; + s[15] = (s5 >> 15) | (s6 * ((uint64_t) 1 << 6)); + s[16] = s6 >> 2; + s[17] = s6 >> 10; + s[18] = (s6 >> 18) | (s7 * ((uint64_t) 1 << 3)); + s[19] = s7 >> 5; + s[20] = s7 >> 13; + s[21] = s8 >> 0; + s[22] = s8 >> 8; + s[23] = (s8 >> 16) | (s9 * ((uint64_t) 1 << 5)); + s[24] = s9 >> 3; + s[25] = s9 >> 11; + s[26] = (s9 >> 19) | (s10 * ((uint64_t) 1 << 2)); + s[27] = s10 >> 6; + s[28] = (s10 >> 14) | (s11 * ((uint64_t) 1 << 7)); + s[29] = s11 >> 1; + s[30] = s11 >> 9; + s[31] = s11 >> 17; +} + +/* + Input: + a[0]+256*a[1]+...+256^31*a[31] = a + b[0]+256*b[1]+...+256^31*b[31] = b + c[0]+256*c[1]+...+256^31*c[31] = c + * + Output: + s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l + where l = 2^252 + 27742317777372353535851937790883648493. + */ + +void +sc25519_muladd(unsigned char s[32], const unsigned char a[32], + const unsigned char b[32], const unsigned char c[32]) +{ + int64_t a0 = 2097151 & load_3(a); + int64_t a1 = 2097151 & (load_4(a + 2) >> 5); + int64_t a2 = 2097151 & (load_3(a + 5) >> 2); + int64_t a3 = 2097151 & (load_4(a + 7) >> 7); + int64_t a4 = 2097151 & (load_4(a + 10) >> 4); + int64_t a5 = 2097151 & (load_3(a + 13) >> 1); + int64_t a6 = 2097151 & (load_4(a + 15) >> 6); + int64_t a7 = 2097151 & (load_3(a + 18) >> 3); + int64_t a8 = 2097151 & load_3(a + 21); + int64_t a9 = 2097151 & (load_4(a + 23) >> 5); + int64_t a10 = 2097151 & (load_3(a + 26) >> 2); + int64_t a11 = (load_4(a + 28) >> 7); + + int64_t b0 = 2097151 & load_3(b); + int64_t b1 = 2097151 & (load_4(b + 2) >> 5); + int64_t b2 = 2097151 & (load_3(b + 5) >> 2); + int64_t b3 = 2097151 & (load_4(b + 7) >> 7); + int64_t b4 = 2097151 & (load_4(b + 10) >> 4); + int64_t b5 = 2097151 & (load_3(b + 13) >> 1); + int64_t b6 = 2097151 & (load_4(b + 15) >> 6); + int64_t b7 = 2097151 & (load_3(b + 18) >> 3); + int64_t b8 = 2097151 & load_3(b + 21); + int64_t b9 = 2097151 & (load_4(b + 23) >> 5); + int64_t b10 = 2097151 & (load_3(b + 26) >> 2); + int64_t b11 = (load_4(b + 28) >> 7); + + int64_t c0 = 2097151 & load_3(c); + int64_t c1 = 2097151 & (load_4(c + 2) >> 5); + int64_t c2 = 2097151 & (load_3(c + 5) >> 2); + int64_t c3 = 2097151 & (load_4(c + 7) >> 7); + int64_t c4 = 2097151 & (load_4(c + 10) >> 4); + int64_t c5 = 2097151 & (load_3(c + 13) >> 1); + int64_t c6 = 2097151 & (load_4(c + 15) >> 6); + int64_t c7 = 2097151 & (load_3(c + 18) >> 3); + int64_t c8 = 2097151 & load_3(c + 21); + int64_t c9 = 2097151 & (load_4(c + 23) >> 5); + int64_t c10 = 2097151 & (load_3(c + 26) >> 2); + int64_t c11 = (load_4(c + 28) >> 7); + + int64_t s0; + int64_t s1; + int64_t s2; + int64_t s3; + int64_t s4; + int64_t s5; + int64_t s6; + int64_t s7; + int64_t s8; + int64_t s9; + int64_t s10; + int64_t s11; + int64_t s12; + int64_t s13; + int64_t s14; + int64_t s15; + int64_t s16; + int64_t s17; + int64_t s18; + int64_t s19; + int64_t s20; + int64_t s21; + int64_t s22; + int64_t s23; + + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + int64_t carry10; + int64_t carry11; + int64_t carry12; + int64_t carry13; + int64_t carry14; + int64_t carry15; + int64_t carry16; + int64_t carry17; + int64_t carry18; + int64_t carry19; + int64_t carry20; + int64_t carry21; + int64_t carry22; + + s0 = c0 + a0 * b0; + s1 = c1 + a0 * b1 + a1 * b0; + s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0; + s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0; + s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0; + s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0; + s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + + a6 * b0; + s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + + a6 * b1 + a7 * b0; + s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + + a6 * b2 + a7 * b1 + a8 * b0; + s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + + a6 * b3 + a7 * b2 + a8 * b1 + a9 * b0; + s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + + a6 * b4 + a7 * b3 + a8 * b2 + a9 * b1 + a10 * b0; + s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + + a6 * b5 + a7 * b4 + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0; + s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + + a7 * b5 + a8 * b4 + a9 * b3 + a10 * b2 + a11 * b1; + s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + + a8 * b5 + a9 * b4 + a10 * b3 + a11 * b2; + s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + + a9 * b5 + a10 * b4 + a11 * b3; + s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + + a10 * b5 + a11 * b4; + s16 = + a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5; + s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6; + s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7; + s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8; + s20 = a9 * b11 + a10 * b10 + a11 * b9; + s21 = a10 * b11 + a11 * b10; + s22 = a11 * b11; + s23 = 0; + + carry0 = (s0 + (int64_t) (1L << 20)) >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry2 = (s2 + (int64_t) (1L << 20)) >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry4 = (s4 + (int64_t) (1L << 20)) >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry12 = (s12 + (int64_t) (1L << 20)) >> 21; + s13 += carry12; + s12 -= carry12 * ((uint64_t) 1L << 21); + carry14 = (s14 + (int64_t) (1L << 20)) >> 21; + s15 += carry14; + s14 -= carry14 * ((uint64_t) 1L << 21); + carry16 = (s16 + (int64_t) (1L << 20)) >> 21; + s17 += carry16; + s16 -= carry16 * ((uint64_t) 1L << 21); + carry18 = (s18 + (int64_t) (1L << 20)) >> 21; + s19 += carry18; + s18 -= carry18 * ((uint64_t) 1L << 21); + carry20 = (s20 + (int64_t) (1L << 20)) >> 21; + s21 += carry20; + s20 -= carry20 * ((uint64_t) 1L << 21); + carry22 = (s22 + (int64_t) (1L << 20)) >> 21; + s23 += carry22; + s22 -= carry22 * ((uint64_t) 1L << 21); + + carry1 = (s1 + (int64_t) (1L << 20)) >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry3 = (s3 + (int64_t) (1L << 20)) >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry5 = (s5 + (int64_t) (1L << 20)) >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + carry13 = (s13 + (int64_t) (1L << 20)) >> 21; + s14 += carry13; + s13 -= carry13 * ((uint64_t) 1L << 21); + carry15 = (s15 + (int64_t) (1L << 20)) >> 21; + s16 += carry15; + s15 -= carry15 * ((uint64_t) 1L << 21); + carry17 = (s17 + (int64_t) (1L << 20)) >> 21; + s18 += carry17; + s17 -= carry17 * ((uint64_t) 1L << 21); + carry19 = (s19 + (int64_t) (1L << 20)) >> 21; + s20 += carry19; + s19 -= carry19 * ((uint64_t) 1L << 21); + carry21 = (s21 + (int64_t) (1L << 20)) >> 21; + s22 += carry21; + s21 -= carry21 * ((uint64_t) 1L << 21); + + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry12 = (s12 + (int64_t) (1L << 20)) >> 21; + s13 += carry12; + s12 -= carry12 * ((uint64_t) 1L << 21); + carry14 = (s14 + (int64_t) (1L << 20)) >> 21; + s15 += carry14; + s14 -= carry14 * ((uint64_t) 1L << 21); + carry16 = (s16 + (int64_t) (1L << 20)) >> 21; + s17 += carry16; + s16 -= carry16 * ((uint64_t) 1L << 21); + + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + carry13 = (s13 + (int64_t) (1L << 20)) >> 21; + s14 += carry13; + s13 -= carry13 * ((uint64_t) 1L << 21); + carry15 = (s15 + (int64_t) (1L << 20)) >> 21; + s16 += carry15; + s15 -= carry15 * ((uint64_t) 1L << 21); + + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = (s0 + (int64_t) (1L << 20)) >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry2 = (s2 + (int64_t) (1L << 20)) >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry4 = (s4 + (int64_t) (1L << 20)) >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + + carry1 = (s1 + (int64_t) (1L << 20)) >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry3 = (s3 + (int64_t) (1L << 20)) >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry5 = (s5 + (int64_t) (1L << 20)) >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry11 = s11 >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + + s[0] = s0 >> 0; + s[1] = s0 >> 8; + s[2] = (s0 >> 16) | (s1 * ((uint64_t) 1 << 5)); + s[3] = s1 >> 3; + s[4] = s1 >> 11; + s[5] = (s1 >> 19) | (s2 * ((uint64_t) 1 << 2)); + s[6] = s2 >> 6; + s[7] = (s2 >> 14) | (s3 * ((uint64_t) 1 << 7)); + s[8] = s3 >> 1; + s[9] = s3 >> 9; + s[10] = (s3 >> 17) | (s4 * ((uint64_t) 1 << 4)); + s[11] = s4 >> 4; + s[12] = s4 >> 12; + s[13] = (s4 >> 20) | (s5 * ((uint64_t) 1 << 1)); + s[14] = s5 >> 7; + s[15] = (s5 >> 15) | (s6 * ((uint64_t) 1 << 6)); + s[16] = s6 >> 2; + s[17] = s6 >> 10; + s[18] = (s6 >> 18) | (s7 * ((uint64_t) 1 << 3)); + s[19] = s7 >> 5; + s[20] = s7 >> 13; + s[21] = s8 >> 0; + s[22] = s8 >> 8; + s[23] = (s8 >> 16) | (s9 * ((uint64_t) 1 << 5)); + s[24] = s9 >> 3; + s[25] = s9 >> 11; + s[26] = (s9 >> 19) | (s10 * ((uint64_t) 1 << 2)); + s[27] = s10 >> 6; + s[28] = (s10 >> 14) | (s11 * ((uint64_t) 1 << 7)); + s[29] = s11 >> 1; + s[30] = s11 >> 9; + s[31] = s11 >> 17; +} + +/* + Input: + a[0]+256*a[1]+...+256^31*a[31] = a + * + Output: + s[0]+256*s[1]+...+256^31*s[31] = a^2 mod l + where l = 2^252 + 27742317777372353535851937790883648493. + */ + +static inline void +sc25519_sq(unsigned char *s, const unsigned char *a) +{ + sc25519_mul(s, a, a); +} + +/* + Input: + s[0]+256*a[1]+...+256^31*a[31] = a + n + * + Output: + s[0]+256*s[1]+...+256^31*s[31] = x * s^(s^n) mod l + where l = 2^252 + 27742317777372353535851937790883648493. + Overwrites s in place. + */ + +static inline void +sc25519_sqmul(unsigned char s[32], const int n, const unsigned char a[32]) +{ + int i; + + for (i = 0; i < n; i++) { + sc25519_sq(s, s); + } + sc25519_mul(s, s, a); +} + +void +sc25519_invert(unsigned char recip[32], const unsigned char s[32]) +{ + unsigned char _10[32], _100[32], _1000[32], _10000[32], _100000[32], + _1000000[32], _10010011[32], _10010111[32], _100110[32], _1010[32], + _1010000[32], _1010011[32], _1011[32], _10110[32], _10111101[32], + _11[32], _1100011[32], _1100111[32], _11010011[32], _1101011[32], + _11100111[32], _11101011[32], _11110101[32]; + + sc25519_sq(_10, s); + sc25519_mul(_11, s, _10); + sc25519_mul(_100, s, _11); + sc25519_sq(_1000, _100); + sc25519_mul(_1010, _10, _1000); + sc25519_mul(_1011, s, _1010); + sc25519_sq(_10000, _1000); + sc25519_sq(_10110, _1011); + sc25519_mul(_100000, _1010, _10110); + sc25519_mul(_100110, _10000, _10110); + sc25519_sq(_1000000, _100000); + sc25519_mul(_1010000, _10000, _1000000); + sc25519_mul(_1010011, _11, _1010000); + sc25519_mul(_1100011, _10000, _1010011); + sc25519_mul(_1100111, _100, _1100011); + sc25519_mul(_1101011, _100, _1100111); + sc25519_mul(_10010011, _1000000, _1010011); + sc25519_mul(_10010111, _100, _10010011); + sc25519_mul(_10111101, _100110, _10010111); + sc25519_mul(_11010011, _10110, _10111101); + sc25519_mul(_11100111, _1010000, _10010111); + sc25519_mul(_11101011, _100, _11100111); + sc25519_mul(_11110101, _1010, _11101011); + + sc25519_mul(recip, _1011, _11110101); + sc25519_sqmul(recip, 126, _1010011); + sc25519_sqmul(recip, 9, _10); + sc25519_mul(recip, recip, _11110101); + sc25519_sqmul(recip, 7, _1100111); + sc25519_sqmul(recip, 9, _11110101); + sc25519_sqmul(recip, 11, _10111101); + sc25519_sqmul(recip, 8, _11100111); + sc25519_sqmul(recip, 9, _1101011); + sc25519_sqmul(recip, 6, _1011); + sc25519_sqmul(recip, 14, _10010011); + sc25519_sqmul(recip, 10, _1100011); + sc25519_sqmul(recip, 9, _10010111); + sc25519_sqmul(recip, 10, _11110101); + sc25519_sqmul(recip, 8, _11010011); + sc25519_sqmul(recip, 8, _11101011); +} + +/* + Input: + s[0]+256*s[1]+...+256^63*s[63] = s + * + Output: + s[0]+256*s[1]+...+256^31*s[31] = s mod l + where l = 2^252 + 27742317777372353535851937790883648493. + Overwrites s in place. + */ + +void +sc25519_reduce(unsigned char s[64]) +{ + int64_t s0 = 2097151 & load_3(s); + int64_t s1 = 2097151 & (load_4(s + 2) >> 5); + int64_t s2 = 2097151 & (load_3(s + 5) >> 2); + int64_t s3 = 2097151 & (load_4(s + 7) >> 7); + int64_t s4 = 2097151 & (load_4(s + 10) >> 4); + int64_t s5 = 2097151 & (load_3(s + 13) >> 1); + int64_t s6 = 2097151 & (load_4(s + 15) >> 6); + int64_t s7 = 2097151 & (load_3(s + 18) >> 3); + int64_t s8 = 2097151 & load_3(s + 21); + int64_t s9 = 2097151 & (load_4(s + 23) >> 5); + int64_t s10 = 2097151 & (load_3(s + 26) >> 2); + int64_t s11 = 2097151 & (load_4(s + 28) >> 7); + int64_t s12 = 2097151 & (load_4(s + 31) >> 4); + int64_t s13 = 2097151 & (load_3(s + 34) >> 1); + int64_t s14 = 2097151 & (load_4(s + 36) >> 6); + int64_t s15 = 2097151 & (load_3(s + 39) >> 3); + int64_t s16 = 2097151 & load_3(s + 42); + int64_t s17 = 2097151 & (load_4(s + 44) >> 5); + int64_t s18 = 2097151 & (load_3(s + 47) >> 2); + int64_t s19 = 2097151 & (load_4(s + 49) >> 7); + int64_t s20 = 2097151 & (load_4(s + 52) >> 4); + int64_t s21 = 2097151 & (load_3(s + 55) >> 1); + int64_t s22 = 2097151 & (load_4(s + 57) >> 6); + int64_t s23 = (load_4(s + 60) >> 3); + + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + int64_t carry10; + int64_t carry11; + int64_t carry12; + int64_t carry13; + int64_t carry14; + int64_t carry15; + int64_t carry16; + + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry12 = (s12 + (int64_t) (1L << 20)) >> 21; + s13 += carry12; + s12 -= carry12 * ((uint64_t) 1L << 21); + carry14 = (s14 + (int64_t) (1L << 20)) >> 21; + s15 += carry14; + s14 -= carry14 * ((uint64_t) 1L << 21); + carry16 = (s16 + (int64_t) (1L << 20)) >> 21; + s17 += carry16; + s16 -= carry16 * ((uint64_t) 1L << 21); + + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + carry13 = (s13 + (int64_t) (1L << 20)) >> 21; + s14 += carry13; + s13 -= carry13 * ((uint64_t) 1L << 21); + carry15 = (s15 + (int64_t) (1L << 20)) >> 21; + s16 += carry15; + s15 -= carry15 * ((uint64_t) 1L << 21); + + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = (s0 + (int64_t) (1L << 20)) >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry2 = (s2 + (int64_t) (1L << 20)) >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry4 = (s4 + (int64_t) (1L << 20)) >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry6 = (s6 + (int64_t) (1L << 20)) >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry8 = (s8 + (int64_t) (1L << 20)) >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry10 = (s10 + (int64_t) (1L << 20)) >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + + carry1 = (s1 + (int64_t) (1L << 20)) >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry3 = (s3 + (int64_t) (1L << 20)) >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry5 = (s5 + (int64_t) (1L << 20)) >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry7 = (s7 + (int64_t) (1L << 20)) >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry9 = (s9 + (int64_t) (1L << 20)) >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry11 = (s11 + (int64_t) (1L << 20)) >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + carry11 = s11 >> 21; + s12 += carry11; + s11 -= carry11 * ((uint64_t) 1L << 21); + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 * ((uint64_t) 1L << 21); + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 * ((uint64_t) 1L << 21); + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 * ((uint64_t) 1L << 21); + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 * ((uint64_t) 1L << 21); + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 * ((uint64_t) 1L << 21); + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 * ((uint64_t) 1L << 21); + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 * ((uint64_t) 1L << 21); + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 * ((uint64_t) 1L << 21); + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 * ((uint64_t) 1L << 21); + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 * ((uint64_t) 1L << 21); + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 * ((uint64_t) 1L << 21); + + s[0] = s0 >> 0; + s[1] = s0 >> 8; + s[2] = (s0 >> 16) | (s1 * ((uint64_t) 1 << 5)); + s[3] = s1 >> 3; + s[4] = s1 >> 11; + s[5] = (s1 >> 19) | (s2 * ((uint64_t) 1 << 2)); + s[6] = s2 >> 6; + s[7] = (s2 >> 14) | (s3 * ((uint64_t) 1 << 7)); + s[8] = s3 >> 1; + s[9] = s3 >> 9; + s[10] = (s3 >> 17) | (s4 * ((uint64_t) 1 << 4)); + s[11] = s4 >> 4; + s[12] = s4 >> 12; + s[13] = (s4 >> 20) | (s5 * ((uint64_t) 1 << 1)); + s[14] = s5 >> 7; + s[15] = (s5 >> 15) | (s6 * ((uint64_t) 1 << 6)); + s[16] = s6 >> 2; + s[17] = s6 >> 10; + s[18] = (s6 >> 18) | (s7 * ((uint64_t) 1 << 3)); + s[19] = s7 >> 5; + s[20] = s7 >> 13; + s[21] = s8 >> 0; + s[22] = s8 >> 8; + s[23] = (s8 >> 16) | (s9 * ((uint64_t) 1 << 5)); + s[24] = s9 >> 3; + s[25] = s9 >> 11; + s[26] = (s9 >> 19) | (s10 * ((uint64_t) 1 << 2)); + s[27] = s10 >> 6; + s[28] = (s10 >> 14) | (s11 * ((uint64_t) 1 << 7)); + s[29] = s11 >> 1; + s[30] = s11 >> 9; + s[31] = s11 >> 17; +} + +int +sc25519_is_canonical(const unsigned char s[32]) +{ + /* 2^252+27742317777372353535851937790883648493 */ + static const unsigned char L[32] = { + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, + 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10 + }; + unsigned char c = 0; + unsigned char n = 1; + unsigned int i = 32; + + do { + i--; + c |= ((s[i] - L[i]) >> 8) & n; + n &= ((s[i] ^ L[i]) - 1) >> 8; + } while (i != 0); + + return (c != 0); +} + +/* multiply by the cofactor */ +static void +ge25519_clear_cofactor(ge25519_p3 *p3) +{ + ge25519_p1p1 p1; + ge25519_p2 p2; + + ge25519_p3_dbl(&p1, p3); + ge25519_p1p1_to_p2(&p2, &p1); + ge25519_p2_dbl(&p1, &p2); + ge25519_p1p1_to_p2(&p2, &p1); + ge25519_p2_dbl(&p1, &p2); + ge25519_p1p1_to_p3(p3, &p1); +} + +static void +ge25519_elligator2(unsigned char s[32], const fe25519 r, const unsigned char x_sign) +{ + fe25519 gx; + fe25519 negx; + fe25519 rr2; + fe25519 x, x2, x3; + ge25519_p3 p3; + unsigned int notsquare; + + fe25519_sq2(rr2, r); + rr2[0]++; + fe25519_invert(rr2, rr2); + fe25519_mul32(x, rr2, curve25519_A[0]); + fe25519_neg(x, x); + + fe25519_sq(x2, x); + fe25519_mul(x3, x, x2); + fe25519_add(gx, x3, x); + fe25519_mul32(x2, x2, curve25519_A[0]); + fe25519_add(gx, x2, gx); + + notsquare = fe25519_notsquare(gx); + fe25519_neg(negx, x); + fe25519_cmov(x, negx, notsquare); + fe25519_0(x2); + fe25519_cmov(x2, curve25519_A, notsquare); + fe25519_sub(x, x, x2); + + /* yed = (x-1)/(x+1) */ + { + fe25519 one; + fe25519 x_plus_one; + fe25519 x_plus_one_inv; + fe25519 x_minus_one; + fe25519 yed; + + fe25519_1(one); + fe25519_add(x_plus_one, x, one); + fe25519_sub(x_minus_one, x, one); + fe25519_invert(x_plus_one_inv, x_plus_one); + fe25519_mul(yed, x_minus_one, x_plus_one_inv); + fe25519_tobytes(s, yed); + } + + /* recover x */ + s[31] |= x_sign; + if (ge25519_frombytes(&p3, s) != 0) { + abort(); /* LCOV_EXCL_LINE */ + } + + ge25519_clear_cofactor(&p3); + ge25519_p3_tobytes(s, &p3); +} + +void +ge25519_from_uniform(unsigned char s[32], const unsigned char r[32]) +{ + fe25519 r_fe; + unsigned char x_sign; + + memcpy(s, r, 32); + x_sign = s[31] & 0x80; + s[31] &= 0x7f; + fe25519_frombytes(r_fe, s); + ge25519_elligator2(s, r_fe, x_sign); +} + +void +ge25519_from_hash(unsigned char s[32], const unsigned char h[64]) +{ + unsigned char fl[32]; + unsigned char gl[32]; + fe25519 fe_f; + fe25519 fe_g; + size_t i; + unsigned char x_sign; + + x_sign = h[0] & 0x80; + for (i = 0; i < 32; i++) { + fl[i] = h[63 - i]; + gl[i] = h[31 - i]; + } + fl[31] &= 0x7f; + gl[31] &= 0x7f; + fe25519_frombytes(fe_f, fl); + fe25519_frombytes(fe_g, gl); + fe_f[0] += (h[32] >> 7) * 19; + for (i = 0; i < sizeof (fe25519) / sizeof fe_f[0]; i++) { + fe_f[i] += 38 * fe_g[i]; + } + fe25519_reduce(fe_f, fe_f); + ge25519_elligator2(s, fe_f, x_sign); +} + +/* Ristretto group */ + +static int +ristretto255_sqrt_ratio_m1(fe25519 x, const fe25519 u, const fe25519 v) +{ + fe25519 v3; + fe25519 vxx; + fe25519 m_root_check, p_root_check, f_root_check; + fe25519 x_sqrtm1; + int has_m_root, has_p_root, has_f_root; + + fe25519_sq(v3, v); + fe25519_mul(v3, v3, v); /* v3 = v^3 */ + fe25519_sq(x, v3); + fe25519_mul(x, x, v); + fe25519_mul(x, x, u); /* x = uv^7 */ + + fe25519_pow22523(x, x); /* x = (uv^7)^((q-5)/8) */ + fe25519_mul(x, x, v3); + fe25519_mul(x, x, u); /* x = uv^3(uv^7)^((q-5)/8) */ + + fe25519_sq(vxx, x); + fe25519_mul(vxx, vxx, v); /* vx^2 */ + fe25519_sub(m_root_check, vxx, u); /* vx^2-u */ + fe25519_add(p_root_check, vxx, u); /* vx^2+u */ + fe25519_mul(f_root_check, u, sqrtm1); /* u*sqrt(-1) */ + fe25519_add(f_root_check, vxx, f_root_check); /* vx^2+u*sqrt(-1) */ + has_m_root = fe25519_iszero(m_root_check); + has_p_root = fe25519_iszero(p_root_check); + has_f_root = fe25519_iszero(f_root_check); + fe25519_mul(x_sqrtm1, x, sqrtm1); /* x*sqrt(-1) */ + + fe25519_cmov(x, x_sqrtm1, has_p_root | has_f_root); + fe25519_abs(x, x); + + return has_m_root | has_p_root; +} + +static int +ristretto255_is_canonical(const unsigned char *s) +{ + unsigned char c; + unsigned char d; + unsigned char e; + unsigned int i; + + c = (s[31] & 0x7f) ^ 0x7f; + for (i = 30; i > 0; i--) { + c |= s[i] ^ 0xff; + } + c = (((unsigned int) c) - 1U) >> 8; + d = (0xed - 1U - (unsigned int) s[0]) >> 8; + e = s[31] >> 7; + + return 1 - (((c & d) | e | s[0]) & 1); +} + +int +ristretto255_frombytes(ge25519_p3 *h, const unsigned char *s) +{ + fe25519 inv_sqrt; + fe25519 one; + fe25519 s_; + fe25519 ss; + fe25519 u1, u2; + fe25519 u1u1, u2u2; + fe25519 v; + fe25519 v_u2u2; + int was_square; + + if (ristretto255_is_canonical(s) == 0) { + return -1; + } + fe25519_frombytes(s_, s); + fe25519_sq(ss, s_); /* ss = s^2 */ + + fe25519_1(u1); + fe25519_sub(u1, u1, ss); /* u1 = 1-ss */ + fe25519_sq(u1u1, u1); /* u1u1 = u1^2 */ + + fe25519_1(u2); + fe25519_add(u2, u2, ss); /* u2 = 1+ss */ + fe25519_sq(u2u2, u2); /* u2u2 = u2^2 */ + + fe25519_mul(v, d, u1u1); /* v = d*u1^2 */ + fe25519_neg(v, v); /* v = -d*u1^2 */ + fe25519_sub(v, v, u2u2); /* v = -(d*u1^2)-u2^2 */ + + fe25519_mul(v_u2u2, v, u2u2); /* v_u2u2 = v*u2^2 */ + + fe25519_1(one); + was_square = ristretto255_sqrt_ratio_m1(inv_sqrt, one, v_u2u2); + fe25519_mul(h->X, inv_sqrt, u2); + fe25519_mul(h->Y, inv_sqrt, h->X); + fe25519_mul(h->Y, h->Y, v); + + fe25519_mul(h->X, h->X, s_); + fe25519_add(h->X, h->X, h->X); + fe25519_abs(h->X, h->X); + fe25519_mul(h->Y, u1, h->Y); + fe25519_1(h->Z); + fe25519_mul(h->T, h->X, h->Y); + + return - ((1 - was_square) | + fe25519_isnegative(h->T) | fe25519_iszero(h->Y)); +} + +void +ristretto255_p3_tobytes(unsigned char *s, const ge25519_p3 *h) +{ + fe25519 den1, den2; + fe25519 den_inv; + fe25519 eden; + fe25519 inv_sqrt; + fe25519 ix, iy; + fe25519 one; + fe25519 s_; + fe25519 t_z_inv; + fe25519 u1, u2; + fe25519 u1_u2u2; + fe25519 x_, y_; + fe25519 x_z_inv; + fe25519 z_inv; + fe25519 zmy; + int rotate; + + fe25519_add(u1, h->Z, h->Y); /* u1 = Z+Y */ + fe25519_sub(zmy, h->Z, h->Y); /* zmy = Z-Y */ + fe25519_mul(u1, u1, zmy); /* u1 = (Z+Y)*(Z-Y) */ + fe25519_mul(u2, h->X, h->Y); /* u2 = X*Y */ + + fe25519_sq(u1_u2u2, u2); /* u1_u2u2 = u2^2 */ + fe25519_mul(u1_u2u2, u1, u1_u2u2); /* u1_u2u2 = u1*u2^2 */ + + fe25519_1(one); + (void) ristretto255_sqrt_ratio_m1(inv_sqrt, one, u1_u2u2); + fe25519_mul(den1, inv_sqrt, u1); /* den1 = inv_sqrt*u1 */ + fe25519_mul(den2, inv_sqrt, u2); /* den2 = inv_sqrt*u2 */ + fe25519_mul(z_inv, den1, den2); /* z_inv = den1*den2 */ + fe25519_mul(z_inv, z_inv, h->T); /* z_inv = den1*den2*T */ + + fe25519_mul(ix, h->X, sqrtm1); /* ix = X*sqrt(-1) */ + fe25519_mul(iy, h->Y, sqrtm1); /* iy = Y*sqrt(-1) */ + fe25519_mul(eden, den1, invsqrtamd); /* eden = den1/sqrt(a-d) */ + + fe25519_mul(t_z_inv, h->T, z_inv); /* t_z_inv = T*z_inv */ + rotate = fe25519_isnegative(t_z_inv); + + fe25519_copy(x_, h->X); + fe25519_copy(y_, h->Y); + fe25519_copy(den_inv, den2); + + fe25519_cmov(x_, iy, rotate); + fe25519_cmov(y_, ix, rotate); + fe25519_cmov(den_inv, eden, rotate); + + fe25519_mul(x_z_inv, x_, z_inv); + fe25519_cneg(y_, y_, fe25519_isnegative(x_z_inv)); + + fe25519_sub(s_, h->Z, y_); + fe25519_mul(s_, den_inv, s_); + fe25519_abs(s_, s_); + fe25519_tobytes(s, s_); +} + +static void +ristretto255_elligator(ge25519_p3 *p, const fe25519 t) +{ + fe25519 c; + fe25519 n; + fe25519 one; + fe25519 r; + fe25519 rpd; + fe25519 s, s_prime; + fe25519 ss; + fe25519 u, v; + fe25519 w0, w1, w2, w3; + int wasnt_square; + + fe25519_1(one); + fe25519_sq(r, t); /* r = t^2 */ + fe25519_mul(r, sqrtm1, r); /* r = sqrt(-1)*t^2 */ + fe25519_add(u, r, one); /* u = r+1 */ + fe25519_mul(u, u, onemsqd); /* u = (r+1)*(1-d^2) */ + fe25519_1(c); + fe25519_neg(c, c); /* c = -1 */ + fe25519_add(rpd, r, d); /* rpd = r+d */ + fe25519_mul(v, r, d); /* v = r*d */ + fe25519_sub(v, c, v); /* v = c-r*d */ + fe25519_mul(v, v, rpd); /* v = (c-r*d)*(r+d) */ + + wasnt_square = 1 - ristretto255_sqrt_ratio_m1(s, u, v); + fe25519_mul(s_prime, s, t); + fe25519_abs(s_prime, s_prime); + fe25519_neg(s_prime, s_prime); /* s_prime = -|s*t| */ + fe25519_cmov(s, s_prime, wasnt_square); + fe25519_cmov(c, r, wasnt_square); + + fe25519_sub(n, r, one); /* n = r-1 */ + fe25519_mul(n, n, c); /* n = c*(r-1) */ + fe25519_mul(n, n, sqdmone); /* n = c*(r-1)*(d-1)^2 */ + fe25519_sub(n, n, v); /* n = c*(r-1)*(d-1)^2-v */ + + fe25519_add(w0, s, s); /* w0 = 2s */ + fe25519_mul(w0, w0, v); /* w0 = 2s*v */ + fe25519_mul(w1, n, sqrtadm1); /* w1 = n*sqrt(ad-1) */ + fe25519_sq(ss, s); /* ss = s^2 */ + fe25519_sub(w2, one, ss); /* w2 = 1-s^2 */ + fe25519_add(w3, one, ss); /* w3 = 1+s^2 */ + + fe25519_mul(p->X, w0, w3); + fe25519_mul(p->Y, w2, w1); + fe25519_mul(p->Z, w1, w3); + fe25519_mul(p->T, w0, w2); +} + +void +ristretto255_from_hash(unsigned char s[32], const unsigned char h[64]) +{ + fe25519 r0, r1; + ge25519_cached p1_cached; + ge25519_p1p1 p_p1p1; + ge25519_p3 p0, p1; + ge25519_p3 p; + + fe25519_frombytes(r0, h); + fe25519_frombytes(r1, h + 32); + ristretto255_elligator(&p0, r0); + ristretto255_elligator(&p1, r1); + ge25519_p3_to_cached(&p1_cached, &p1); + ge25519_add(&p_p1p1, &p0, &p1_cached); + ge25519_p1p1_to_p3(&p, &p_p1p1); + ristretto255_p3_tobytes(s, &p); +} diff --git a/packages/runtime/vendor/libsodium/ed25519_ref10.h b/packages/runtime/vendor/libsodium/ed25519_ref10.h new file mode 100644 index 0000000000..3f4c45caf0 --- /dev/null +++ b/packages/runtime/vendor/libsodium/ed25519_ref10.h @@ -0,0 +1,142 @@ +#ifndef ed25519_ref10_H +#define ed25519_ref10_H + +#include +#include + +/* + fe means field element. + Here the field is \Z/(2^255-19). + */ + +#ifdef HAVE_TI_MODE +typedef uint64_t fe25519[5]; +#else +typedef int32_t fe25519[10]; +#endif + +void fe25519_invert(fe25519 out, const fe25519 z); +void fe25519_frombytes(fe25519 h, const unsigned char *s); +void fe25519_tobytes(unsigned char *s, const fe25519 h); + +#ifdef HAVE_TI_MODE +# include "ed25519_ref10_fe_51.h" +#else +# include "ed25519_ref10_fe_25_5.h" +#endif + + +/* + ge means group element. + + Here the group is the set of pairs (x,y) of field elements + satisfying -x^2 + y^2 = 1 + d x^2y^2 + where d = -121665/121666. + + Representations: + ge25519_p2 (projective): (X:Y:Z) satisfying x=X/Z, y=Y/Z + ge25519_p3 (extended): (X:Y:Z:T) satisfying x=X/Z, y=Y/Z, XY=ZT + ge25519_p1p1 (completed): ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T + ge25519_precomp (Duif): (y+x,y-x,2dxy) + */ + +typedef struct { + fe25519 X; + fe25519 Y; + fe25519 Z; +} ge25519_p2; + +typedef struct { + fe25519 X; + fe25519 Y; + fe25519 Z; + fe25519 T; +} ge25519_p3; + +typedef struct { + fe25519 X; + fe25519 Y; + fe25519 Z; + fe25519 T; +} ge25519_p1p1; + +typedef struct { + fe25519 yplusx; + fe25519 yminusx; + fe25519 xy2d; +} ge25519_precomp; + +typedef struct { + fe25519 YplusX; + fe25519 YminusX; + fe25519 Z; + fe25519 T2d; +} ge25519_cached; + +void ge25519_tobytes(unsigned char *s, const ge25519_p2 *h); + +void ge25519_p3_tobytes(unsigned char *s, const ge25519_p3 *h); + +int ge25519_frombytes(ge25519_p3 *h, const unsigned char *s); + +int ge25519_frombytes_negate_vartime(ge25519_p3 *h, const unsigned char *s); + +void ge25519_p3_to_cached(ge25519_cached *r, const ge25519_p3 *p); + +void ge25519_p1p1_to_p2(ge25519_p2 *r, const ge25519_p1p1 *p); + +void ge25519_p1p1_to_p3(ge25519_p3 *r, const ge25519_p1p1 *p); + +void ge25519_add(ge25519_p1p1 *r, const ge25519_p3 *p, const ge25519_cached *q); + +void ge25519_sub(ge25519_p1p1 *r, const ge25519_p3 *p, const ge25519_cached *q); + +void ge25519_scalarmult_base(ge25519_p3 *h, const unsigned char *a); + +void ge25519_double_scalarmult_vartime(ge25519_p2 *r, const unsigned char *a, + const ge25519_p3 *A, + const unsigned char *b); + +void ge25519_scalarmult(ge25519_p3 *h, const unsigned char *a, + const ge25519_p3 *p); + +int ge25519_is_canonical(const unsigned char *s); + +int ge25519_is_on_curve(const ge25519_p3 *p); + +int ge25519_is_on_main_subgroup(const ge25519_p3 *p); + +int ge25519_has_small_order(const unsigned char s[32]); + +void ge25519_from_uniform(unsigned char s[32], const unsigned char r[32]); + +void ge25519_from_hash(unsigned char s[32], const unsigned char h[64]); + +/* + Ristretto group + */ + +int ristretto255_frombytes(ge25519_p3 *h, const unsigned char *s); + +void ristretto255_p3_tobytes(unsigned char *s, const ge25519_p3 *h); + +void ristretto255_from_hash(unsigned char s[32], const unsigned char h[64]); + +/* + The set of scalars is \Z/l + where l = 2^252 + 27742317777372353535851937790883648493. + */ + +void sc25519_invert(unsigned char recip[32], const unsigned char s[32]); + +void sc25519_reduce(unsigned char s[64]); + +void sc25519_mul(unsigned char s[32], const unsigned char a[32], + const unsigned char b[32]); + +void sc25519_muladd(unsigned char s[32], const unsigned char a[32], + const unsigned char b[32], const unsigned char c[32]); + +int sc25519_is_canonical(const unsigned char s[32]); + +#endif diff --git a/packages/runtime/vendor/libsodium/ed25519_ref10_fe_25_5.h b/packages/runtime/vendor/libsodium/ed25519_ref10_fe_25_5.h new file mode 100644 index 0000000000..a0816b2198 --- /dev/null +++ b/packages/runtime/vendor/libsodium/ed25519_ref10_fe_25_5.h @@ -0,0 +1,1030 @@ +#include + +#include "common.h" +#include "utils.h" + +/* + h = 0 + */ + +static inline void +fe25519_0(fe25519 h) +{ + memset(&h[0], 0, 10 * sizeof h[0]); +} + +/* + h = 1 + */ + +static inline void +fe25519_1(fe25519 h) +{ + h[0] = 1; + h[1] = 0; + memset(&h[2], 0, 8 * sizeof h[0]); +} + +/* + h = f + g + Can overlap h with f or g. + * + Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + * + Postconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + */ + +static inline void +fe25519_add(fe25519 h, const fe25519 f, const fe25519 g) +{ + int32_t h0 = f[0] + g[0]; + int32_t h1 = f[1] + g[1]; + int32_t h2 = f[2] + g[2]; + int32_t h3 = f[3] + g[3]; + int32_t h4 = f[4] + g[4]; + int32_t h5 = f[5] + g[5]; + int32_t h6 = f[6] + g[6]; + int32_t h7 = f[7] + g[7]; + int32_t h8 = f[8] + g[8]; + int32_t h9 = f[9] + g[9]; + + h[0] = h0; + h[1] = h1; + h[2] = h2; + h[3] = h3; + h[4] = h4; + h[5] = h5; + h[6] = h6; + h[7] = h7; + h[8] = h8; + h[9] = h9; +} + +/* + h = f - g + Can overlap h with f or g. + * + Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + * + Postconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + */ + +static void +fe25519_sub(fe25519 h, const fe25519 f, const fe25519 g) +{ + int32_t h0 = f[0] - g[0]; + int32_t h1 = f[1] - g[1]; + int32_t h2 = f[2] - g[2]; + int32_t h3 = f[3] - g[3]; + int32_t h4 = f[4] - g[4]; + int32_t h5 = f[5] - g[5]; + int32_t h6 = f[6] - g[6]; + int32_t h7 = f[7] - g[7]; + int32_t h8 = f[8] - g[8]; + int32_t h9 = f[9] - g[9]; + + h[0] = h0; + h[1] = h1; + h[2] = h2; + h[3] = h3; + h[4] = h4; + h[5] = h5; + h[6] = h6; + h[7] = h7; + h[8] = h8; + h[9] = h9; +} + +/* + h = -f + * + Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + * + Postconditions: + |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + */ + +static inline void +fe25519_neg(fe25519 h, const fe25519 f) +{ + int32_t h0 = -f[0]; + int32_t h1 = -f[1]; + int32_t h2 = -f[2]; + int32_t h3 = -f[3]; + int32_t h4 = -f[4]; + int32_t h5 = -f[5]; + int32_t h6 = -f[6]; + int32_t h7 = -f[7]; + int32_t h8 = -f[8]; + int32_t h9 = -f[9]; + + h[0] = h0; + h[1] = h1; + h[2] = h2; + h[3] = h3; + h[4] = h4; + h[5] = h5; + h[6] = h6; + h[7] = h7; + h[8] = h8; + h[9] = h9; +} + +/* + Replace (f,g) with (g,g) if b == 1; + replace (f,g) with (f,g) if b == 0. + * + Preconditions: b in {0,1}. + */ + +static void +fe25519_cmov(fe25519 f, const fe25519 g, unsigned int b) +{ + const uint32_t mask = (uint32_t) (-(int32_t) b); + + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + + int32_t x0 = f0 ^ g[0]; + int32_t x1 = f1 ^ g[1]; + int32_t x2 = f2 ^ g[2]; + int32_t x3 = f3 ^ g[3]; + int32_t x4 = f4 ^ g[4]; + int32_t x5 = f5 ^ g[5]; + int32_t x6 = f6 ^ g[6]; + int32_t x7 = f7 ^ g[7]; + int32_t x8 = f8 ^ g[8]; + int32_t x9 = f9 ^ g[9]; + + x0 &= mask; + x1 &= mask; + x2 &= mask; + x3 &= mask; + x4 &= mask; + x5 &= mask; + x6 &= mask; + x7 &= mask; + x8 &= mask; + x9 &= mask; + + f[0] = f0 ^ x0; + f[1] = f1 ^ x1; + f[2] = f2 ^ x2; + f[3] = f3 ^ x3; + f[4] = f4 ^ x4; + f[5] = f5 ^ x5; + f[6] = f6 ^ x6; + f[7] = f7 ^ x7; + f[8] = f8 ^ x8; + f[9] = f9 ^ x9; +} + +static void +fe25519_cswap(fe25519 f, fe25519 g, unsigned int b) +{ + const uint32_t mask = (uint32_t) (-(int64_t) b); + + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + + int32_t g0 = g[0]; + int32_t g1 = g[1]; + int32_t g2 = g[2]; + int32_t g3 = g[3]; + int32_t g4 = g[4]; + int32_t g5 = g[5]; + int32_t g6 = g[6]; + int32_t g7 = g[7]; + int32_t g8 = g[8]; + int32_t g9 = g[9]; + + int32_t x0 = f0 ^ g0; + int32_t x1 = f1 ^ g1; + int32_t x2 = f2 ^ g2; + int32_t x3 = f3 ^ g3; + int32_t x4 = f4 ^ g4; + int32_t x5 = f5 ^ g5; + int32_t x6 = f6 ^ g6; + int32_t x7 = f7 ^ g7; + int32_t x8 = f8 ^ g8; + int32_t x9 = f9 ^ g9; + + x0 &= mask; + x1 &= mask; + x2 &= mask; + x3 &= mask; + x4 &= mask; + x5 &= mask; + x6 &= mask; + x7 &= mask; + x8 &= mask; + x9 &= mask; + + f[0] = f0 ^ x0; + f[1] = f1 ^ x1; + f[2] = f2 ^ x2; + f[3] = f3 ^ x3; + f[4] = f4 ^ x4; + f[5] = f5 ^ x5; + f[6] = f6 ^ x6; + f[7] = f7 ^ x7; + f[8] = f8 ^ x8; + f[9] = f9 ^ x9; + + g[0] = g0 ^ x0; + g[1] = g1 ^ x1; + g[2] = g2 ^ x2; + g[3] = g3 ^ x3; + g[4] = g4 ^ x4; + g[5] = g5 ^ x5; + g[6] = g6 ^ x6; + g[7] = g7 ^ x7; + g[8] = g8 ^ x8; + g[9] = g9 ^ x9; +} + +/* + h = f + */ + +static inline void +fe25519_copy(fe25519 h, const fe25519 f) +{ + memcpy(h, f, 10 * sizeof h[0]); +} + +/* + return 1 if f is in {1,3,5,...,q-2} + return 0 if f is in {0,2,4,...,q-1} + + Preconditions: + |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + */ + +static inline int +fe25519_isnegative(const fe25519 f) +{ + unsigned char s[32]; + + fe25519_tobytes(s, f); + + return s[0] & 1; +} + +/* + return 1 if f == 0 + return 0 if f != 0 + + Preconditions: + |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + */ + +static inline int +fe25519_iszero(const fe25519 f) +{ + unsigned char s[32]; + + fe25519_tobytes(s, f); + + return sodium_is_zero(s, 32); +} + +/* + h = f * g + Can overlap h with f or g. + * + Preconditions: + |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + |g| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + * + Postconditions: + |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. + */ + +/* + Notes on implementation strategy: + * + Using schoolbook multiplication. + Karatsuba would save a little in some cost models. + * + Most multiplications by 2 and 19 are 32-bit precomputations; + cheaper than 64-bit postcomputations. + * + There is one remaining multiplication by 19 in the carry chain; + one *19 precomputation can be merged into this, + but the resulting data flow is considerably less clean. + * + There are 12 carries below. + 10 of them are 2-way parallelizable and vectorizable. + Can get away with 11 carries, but then data flow is much deeper. + * + With tighter constraints on inputs can squeeze carries into int32. + */ + +static void +fe25519_mul(fe25519 h, const fe25519 f, const fe25519 g) +{ + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + + int32_t g0 = g[0]; + int32_t g1 = g[1]; + int32_t g2 = g[2]; + int32_t g3 = g[3]; + int32_t g4 = g[4]; + int32_t g5 = g[5]; + int32_t g6 = g[6]; + int32_t g7 = g[7]; + int32_t g8 = g[8]; + int32_t g9 = g[9]; + + int32_t g1_19 = 19 * g1; /* 1.959375*2^29 */ + int32_t g2_19 = 19 * g2; /* 1.959375*2^30; still ok */ + int32_t g3_19 = 19 * g3; + int32_t g4_19 = 19 * g4; + int32_t g5_19 = 19 * g5; + int32_t g6_19 = 19 * g6; + int32_t g7_19 = 19 * g7; + int32_t g8_19 = 19 * g8; + int32_t g9_19 = 19 * g9; + int32_t f1_2 = 2 * f1; + int32_t f3_2 = 2 * f3; + int32_t f5_2 = 2 * f5; + int32_t f7_2 = 2 * f7; + int32_t f9_2 = 2 * f9; + + int64_t f0g0 = f0 * (int64_t) g0; + int64_t f0g1 = f0 * (int64_t) g1; + int64_t f0g2 = f0 * (int64_t) g2; + int64_t f0g3 = f0 * (int64_t) g3; + int64_t f0g4 = f0 * (int64_t) g4; + int64_t f0g5 = f0 * (int64_t) g5; + int64_t f0g6 = f0 * (int64_t) g6; + int64_t f0g7 = f0 * (int64_t) g7; + int64_t f0g8 = f0 * (int64_t) g8; + int64_t f0g9 = f0 * (int64_t) g9; + int64_t f1g0 = f1 * (int64_t) g0; + int64_t f1g1_2 = f1_2 * (int64_t) g1; + int64_t f1g2 = f1 * (int64_t) g2; + int64_t f1g3_2 = f1_2 * (int64_t) g3; + int64_t f1g4 = f1 * (int64_t) g4; + int64_t f1g5_2 = f1_2 * (int64_t) g5; + int64_t f1g6 = f1 * (int64_t) g6; + int64_t f1g7_2 = f1_2 * (int64_t) g7; + int64_t f1g8 = f1 * (int64_t) g8; + int64_t f1g9_38 = f1_2 * (int64_t) g9_19; + int64_t f2g0 = f2 * (int64_t) g0; + int64_t f2g1 = f2 * (int64_t) g1; + int64_t f2g2 = f2 * (int64_t) g2; + int64_t f2g3 = f2 * (int64_t) g3; + int64_t f2g4 = f2 * (int64_t) g4; + int64_t f2g5 = f2 * (int64_t) g5; + int64_t f2g6 = f2 * (int64_t) g6; + int64_t f2g7 = f2 * (int64_t) g7; + int64_t f2g8_19 = f2 * (int64_t) g8_19; + int64_t f2g9_19 = f2 * (int64_t) g9_19; + int64_t f3g0 = f3 * (int64_t) g0; + int64_t f3g1_2 = f3_2 * (int64_t) g1; + int64_t f3g2 = f3 * (int64_t) g2; + int64_t f3g3_2 = f3_2 * (int64_t) g3; + int64_t f3g4 = f3 * (int64_t) g4; + int64_t f3g5_2 = f3_2 * (int64_t) g5; + int64_t f3g6 = f3 * (int64_t) g6; + int64_t f3g7_38 = f3_2 * (int64_t) g7_19; + int64_t f3g8_19 = f3 * (int64_t) g8_19; + int64_t f3g9_38 = f3_2 * (int64_t) g9_19; + int64_t f4g0 = f4 * (int64_t) g0; + int64_t f4g1 = f4 * (int64_t) g1; + int64_t f4g2 = f4 * (int64_t) g2; + int64_t f4g3 = f4 * (int64_t) g3; + int64_t f4g4 = f4 * (int64_t) g4; + int64_t f4g5 = f4 * (int64_t) g5; + int64_t f4g6_19 = f4 * (int64_t) g6_19; + int64_t f4g7_19 = f4 * (int64_t) g7_19; + int64_t f4g8_19 = f4 * (int64_t) g8_19; + int64_t f4g9_19 = f4 * (int64_t) g9_19; + int64_t f5g0 = f5 * (int64_t) g0; + int64_t f5g1_2 = f5_2 * (int64_t) g1; + int64_t f5g2 = f5 * (int64_t) g2; + int64_t f5g3_2 = f5_2 * (int64_t) g3; + int64_t f5g4 = f5 * (int64_t) g4; + int64_t f5g5_38 = f5_2 * (int64_t) g5_19; + int64_t f5g6_19 = f5 * (int64_t) g6_19; + int64_t f5g7_38 = f5_2 * (int64_t) g7_19; + int64_t f5g8_19 = f5 * (int64_t) g8_19; + int64_t f5g9_38 = f5_2 * (int64_t) g9_19; + int64_t f6g0 = f6 * (int64_t) g0; + int64_t f6g1 = f6 * (int64_t) g1; + int64_t f6g2 = f6 * (int64_t) g2; + int64_t f6g3 = f6 * (int64_t) g3; + int64_t f6g4_19 = f6 * (int64_t) g4_19; + int64_t f6g5_19 = f6 * (int64_t) g5_19; + int64_t f6g6_19 = f6 * (int64_t) g6_19; + int64_t f6g7_19 = f6 * (int64_t) g7_19; + int64_t f6g8_19 = f6 * (int64_t) g8_19; + int64_t f6g9_19 = f6 * (int64_t) g9_19; + int64_t f7g0 = f7 * (int64_t) g0; + int64_t f7g1_2 = f7_2 * (int64_t) g1; + int64_t f7g2 = f7 * (int64_t) g2; + int64_t f7g3_38 = f7_2 * (int64_t) g3_19; + int64_t f7g4_19 = f7 * (int64_t) g4_19; + int64_t f7g5_38 = f7_2 * (int64_t) g5_19; + int64_t f7g6_19 = f7 * (int64_t) g6_19; + int64_t f7g7_38 = f7_2 * (int64_t) g7_19; + int64_t f7g8_19 = f7 * (int64_t) g8_19; + int64_t f7g9_38 = f7_2 * (int64_t) g9_19; + int64_t f8g0 = f8 * (int64_t) g0; + int64_t f8g1 = f8 * (int64_t) g1; + int64_t f8g2_19 = f8 * (int64_t) g2_19; + int64_t f8g3_19 = f8 * (int64_t) g3_19; + int64_t f8g4_19 = f8 * (int64_t) g4_19; + int64_t f8g5_19 = f8 * (int64_t) g5_19; + int64_t f8g6_19 = f8 * (int64_t) g6_19; + int64_t f8g7_19 = f8 * (int64_t) g7_19; + int64_t f8g8_19 = f8 * (int64_t) g8_19; + int64_t f8g9_19 = f8 * (int64_t) g9_19; + int64_t f9g0 = f9 * (int64_t) g0; + int64_t f9g1_38 = f9_2 * (int64_t) g1_19; + int64_t f9g2_19 = f9 * (int64_t) g2_19; + int64_t f9g3_38 = f9_2 * (int64_t) g3_19; + int64_t f9g4_19 = f9 * (int64_t) g4_19; + int64_t f9g5_38 = f9_2 * (int64_t) g5_19; + int64_t f9g6_19 = f9 * (int64_t) g6_19; + int64_t f9g7_38 = f9_2 * (int64_t) g7_19; + int64_t f9g8_19 = f9 * (int64_t) g8_19; + int64_t f9g9_38 = f9_2 * (int64_t) g9_19; + + int64_t h0 = f0g0 + f1g9_38 + f2g8_19 + f3g7_38 + f4g6_19 + f5g5_38 + + f6g4_19 + f7g3_38 + f8g2_19 + f9g1_38; + int64_t h1 = f0g1 + f1g0 + f2g9_19 + f3g8_19 + f4g7_19 + f5g6_19 + f6g5_19 + + f7g4_19 + f8g3_19 + f9g2_19; + int64_t h2 = f0g2 + f1g1_2 + f2g0 + f3g9_38 + f4g8_19 + f5g7_38 + f6g6_19 + + f7g5_38 + f8g4_19 + f9g3_38; + int64_t h3 = f0g3 + f1g2 + f2g1 + f3g0 + f4g9_19 + f5g8_19 + f6g7_19 + + f7g6_19 + f8g5_19 + f9g4_19; + int64_t h4 = f0g4 + f1g3_2 + f2g2 + f3g1_2 + f4g0 + f5g9_38 + f6g8_19 + + f7g7_38 + f8g6_19 + f9g5_38; + int64_t h5 = f0g5 + f1g4 + f2g3 + f3g2 + f4g1 + f5g0 + f6g9_19 + f7g8_19 + + f8g7_19 + f9g6_19; + int64_t h6 = f0g6 + f1g5_2 + f2g4 + f3g3_2 + f4g2 + f5g1_2 + f6g0 + + f7g9_38 + f8g8_19 + f9g7_38; + int64_t h7 = f0g7 + f1g6 + f2g5 + f3g4 + f4g3 + f5g2 + f6g1 + f7g0 + + f8g9_19 + f9g8_19; + int64_t h8 = f0g8 + f1g7_2 + f2g6 + f3g5_2 + f4g4 + f5g3_2 + f6g2 + f7g1_2 + + f8g0 + f9g9_38; + int64_t h9 = + f0g9 + f1g8 + f2g7 + f3g6 + f4g5 + f5g4 + f6g3 + f7g2 + f8g1 + f9g0; + + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + /* + |h0| <= (1.65*1.65*2^52*(1+19+19+19+19)+1.65*1.65*2^50*(38+38+38+38+38)) + i.e. |h0| <= 1.4*2^60; narrower ranges for h2, h4, h6, h8 + |h1| <= (1.65*1.65*2^51*(1+1+19+19+19+19+19+19+19+19)) + i.e. |h1| <= 1.7*2^59; narrower ranges for h3, h5, h7, h9 + */ + + carry0 = (h0 + (int64_t)(1L << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((uint64_t) 1L << 26); + carry4 = (h4 + (int64_t)(1L << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((uint64_t) 1L << 26); + /* |h0| <= 2^25 */ + /* |h4| <= 2^25 */ + /* |h1| <= 1.71*2^59 */ + /* |h5| <= 1.71*2^59 */ + + carry1 = (h1 + (int64_t)(1L << 24)) >> 25; + h2 += carry1; + h1 -= carry1 * ((uint64_t) 1L << 25); + carry5 = (h5 + (int64_t)(1L << 24)) >> 25; + h6 += carry5; + h5 -= carry5 * ((uint64_t) 1L << 25); + /* |h1| <= 2^24; from now on fits into int32 */ + /* |h5| <= 2^24; from now on fits into int32 */ + /* |h2| <= 1.41*2^60 */ + /* |h6| <= 1.41*2^60 */ + + carry2 = (h2 + (int64_t)(1L << 25)) >> 26; + h3 += carry2; + h2 -= carry2 * ((uint64_t) 1L << 26); + carry6 = (h6 + (int64_t)(1L << 25)) >> 26; + h7 += carry6; + h6 -= carry6 * ((uint64_t) 1L << 26); + /* |h2| <= 2^25; from now on fits into int32 unchanged */ + /* |h6| <= 2^25; from now on fits into int32 unchanged */ + /* |h3| <= 1.71*2^59 */ + /* |h7| <= 1.71*2^59 */ + + carry3 = (h3 + (int64_t)(1L << 24)) >> 25; + h4 += carry3; + h3 -= carry3 * ((uint64_t) 1L << 25); + carry7 = (h7 + (int64_t)(1L << 24)) >> 25; + h8 += carry7; + h7 -= carry7 * ((uint64_t) 1L << 25); + /* |h3| <= 2^24; from now on fits into int32 unchanged */ + /* |h7| <= 2^24; from now on fits into int32 unchanged */ + /* |h4| <= 1.72*2^34 */ + /* |h8| <= 1.41*2^60 */ + + carry4 = (h4 + (int64_t)(1L << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((uint64_t) 1L << 26); + carry8 = (h8 + (int64_t)(1L << 25)) >> 26; + h9 += carry8; + h8 -= carry8 * ((uint64_t) 1L << 26); + /* |h4| <= 2^25; from now on fits into int32 unchanged */ + /* |h8| <= 2^25; from now on fits into int32 unchanged */ + /* |h5| <= 1.01*2^24 */ + /* |h9| <= 1.71*2^59 */ + + carry9 = (h9 + (int64_t)(1L << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 * ((uint64_t) 1L << 25); + /* |h9| <= 2^24; from now on fits into int32 unchanged */ + /* |h0| <= 1.1*2^39 */ + + carry0 = (h0 + (int64_t)(1L << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((uint64_t) 1L << 26); + /* |h0| <= 2^25; from now on fits into int32 unchanged */ + /* |h1| <= 1.01*2^24 */ + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + +/* + h = f * f + Can overlap h with f. + * + Preconditions: + |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + * + Postconditions: + |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. + */ + +static void +fe25519_sq(fe25519 h, const fe25519 f) +{ + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + + int32_t f0_2 = 2 * f0; + int32_t f1_2 = 2 * f1; + int32_t f2_2 = 2 * f2; + int32_t f3_2 = 2 * f3; + int32_t f4_2 = 2 * f4; + int32_t f5_2 = 2 * f5; + int32_t f6_2 = 2 * f6; + int32_t f7_2 = 2 * f7; + int32_t f5_38 = 38 * f5; /* 1.959375*2^30 */ + int32_t f6_19 = 19 * f6; /* 1.959375*2^30 */ + int32_t f7_38 = 38 * f7; /* 1.959375*2^30 */ + int32_t f8_19 = 19 * f8; /* 1.959375*2^30 */ + int32_t f9_38 = 38 * f9; /* 1.959375*2^30 */ + + int64_t f0f0 = f0 * (int64_t) f0; + int64_t f0f1_2 = f0_2 * (int64_t) f1; + int64_t f0f2_2 = f0_2 * (int64_t) f2; + int64_t f0f3_2 = f0_2 * (int64_t) f3; + int64_t f0f4_2 = f0_2 * (int64_t) f4; + int64_t f0f5_2 = f0_2 * (int64_t) f5; + int64_t f0f6_2 = f0_2 * (int64_t) f6; + int64_t f0f7_2 = f0_2 * (int64_t) f7; + int64_t f0f8_2 = f0_2 * (int64_t) f8; + int64_t f0f9_2 = f0_2 * (int64_t) f9; + int64_t f1f1_2 = f1_2 * (int64_t) f1; + int64_t f1f2_2 = f1_2 * (int64_t) f2; + int64_t f1f3_4 = f1_2 * (int64_t) f3_2; + int64_t f1f4_2 = f1_2 * (int64_t) f4; + int64_t f1f5_4 = f1_2 * (int64_t) f5_2; + int64_t f1f6_2 = f1_2 * (int64_t) f6; + int64_t f1f7_4 = f1_2 * (int64_t) f7_2; + int64_t f1f8_2 = f1_2 * (int64_t) f8; + int64_t f1f9_76 = f1_2 * (int64_t) f9_38; + int64_t f2f2 = f2 * (int64_t) f2; + int64_t f2f3_2 = f2_2 * (int64_t) f3; + int64_t f2f4_2 = f2_2 * (int64_t) f4; + int64_t f2f5_2 = f2_2 * (int64_t) f5; + int64_t f2f6_2 = f2_2 * (int64_t) f6; + int64_t f2f7_2 = f2_2 * (int64_t) f7; + int64_t f2f8_38 = f2_2 * (int64_t) f8_19; + int64_t f2f9_38 = f2 * (int64_t) f9_38; + int64_t f3f3_2 = f3_2 * (int64_t) f3; + int64_t f3f4_2 = f3_2 * (int64_t) f4; + int64_t f3f5_4 = f3_2 * (int64_t) f5_2; + int64_t f3f6_2 = f3_2 * (int64_t) f6; + int64_t f3f7_76 = f3_2 * (int64_t) f7_38; + int64_t f3f8_38 = f3_2 * (int64_t) f8_19; + int64_t f3f9_76 = f3_2 * (int64_t) f9_38; + int64_t f4f4 = f4 * (int64_t) f4; + int64_t f4f5_2 = f4_2 * (int64_t) f5; + int64_t f4f6_38 = f4_2 * (int64_t) f6_19; + int64_t f4f7_38 = f4 * (int64_t) f7_38; + int64_t f4f8_38 = f4_2 * (int64_t) f8_19; + int64_t f4f9_38 = f4 * (int64_t) f9_38; + int64_t f5f5_38 = f5 * (int64_t) f5_38; + int64_t f5f6_38 = f5_2 * (int64_t) f6_19; + int64_t f5f7_76 = f5_2 * (int64_t) f7_38; + int64_t f5f8_38 = f5_2 * (int64_t) f8_19; + int64_t f5f9_76 = f5_2 * (int64_t) f9_38; + int64_t f6f6_19 = f6 * (int64_t) f6_19; + int64_t f6f7_38 = f6 * (int64_t) f7_38; + int64_t f6f8_38 = f6_2 * (int64_t) f8_19; + int64_t f6f9_38 = f6 * (int64_t) f9_38; + int64_t f7f7_38 = f7 * (int64_t) f7_38; + int64_t f7f8_38 = f7_2 * (int64_t) f8_19; + int64_t f7f9_76 = f7_2 * (int64_t) f9_38; + int64_t f8f8_19 = f8 * (int64_t) f8_19; + int64_t f8f9_38 = f8 * (int64_t) f9_38; + int64_t f9f9_38 = f9 * (int64_t) f9_38; + + int64_t h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38; + int64_t h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38; + int64_t h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19; + int64_t h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38; + int64_t h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38; + int64_t h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38; + int64_t h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19; + int64_t h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38; + int64_t h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38; + int64_t h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2; + + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + carry0 = (h0 + (int64_t)(1L << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((uint64_t) 1L << 26); + carry4 = (h4 + (int64_t)(1L << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((uint64_t) 1L << 26); + + carry1 = (h1 + (int64_t)(1L << 24)) >> 25; + h2 += carry1; + h1 -= carry1 * ((uint64_t) 1L << 25); + carry5 = (h5 + (int64_t)(1L << 24)) >> 25; + h6 += carry5; + h5 -= carry5 * ((uint64_t) 1L << 25); + + carry2 = (h2 + (int64_t)(1L << 25)) >> 26; + h3 += carry2; + h2 -= carry2 * ((uint64_t) 1L << 26); + carry6 = (h6 + (int64_t)(1L << 25)) >> 26; + h7 += carry6; + h6 -= carry6 * ((uint64_t) 1L << 26); + + carry3 = (h3 + (int64_t)(1L << 24)) >> 25; + h4 += carry3; + h3 -= carry3 * ((uint64_t) 1L << 25); + carry7 = (h7 + (int64_t)(1L << 24)) >> 25; + h8 += carry7; + h7 -= carry7 * ((uint64_t) 1L << 25); + + carry4 = (h4 + (int64_t)(1L << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((uint64_t) 1L << 26); + carry8 = (h8 + (int64_t)(1L << 25)) >> 26; + h9 += carry8; + h8 -= carry8 * ((uint64_t) 1L << 26); + + carry9 = (h9 + (int64_t)(1L << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 * ((uint64_t) 1L << 25); + + carry0 = (h0 + (int64_t)(1L << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((uint64_t) 1L << 26); + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + +/* + h = 2 * f * f + Can overlap h with f. + * + Preconditions: + |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + * + Postconditions: + |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. + */ + +static void +fe25519_sq2(fe25519 h, const fe25519 f) +{ + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + + int32_t f0_2 = 2 * f0; + int32_t f1_2 = 2 * f1; + int32_t f2_2 = 2 * f2; + int32_t f3_2 = 2 * f3; + int32_t f4_2 = 2 * f4; + int32_t f5_2 = 2 * f5; + int32_t f6_2 = 2 * f6; + int32_t f7_2 = 2 * f7; + int32_t f5_38 = 38 * f5; /* 1.959375*2^30 */ + int32_t f6_19 = 19 * f6; /* 1.959375*2^30 */ + int32_t f7_38 = 38 * f7; /* 1.959375*2^30 */ + int32_t f8_19 = 19 * f8; /* 1.959375*2^30 */ + int32_t f9_38 = 38 * f9; /* 1.959375*2^30 */ + + int64_t f0f0 = f0 * (int64_t) f0; + int64_t f0f1_2 = f0_2 * (int64_t) f1; + int64_t f0f2_2 = f0_2 * (int64_t) f2; + int64_t f0f3_2 = f0_2 * (int64_t) f3; + int64_t f0f4_2 = f0_2 * (int64_t) f4; + int64_t f0f5_2 = f0_2 * (int64_t) f5; + int64_t f0f6_2 = f0_2 * (int64_t) f6; + int64_t f0f7_2 = f0_2 * (int64_t) f7; + int64_t f0f8_2 = f0_2 * (int64_t) f8; + int64_t f0f9_2 = f0_2 * (int64_t) f9; + int64_t f1f1_2 = f1_2 * (int64_t) f1; + int64_t f1f2_2 = f1_2 * (int64_t) f2; + int64_t f1f3_4 = f1_2 * (int64_t) f3_2; + int64_t f1f4_2 = f1_2 * (int64_t) f4; + int64_t f1f5_4 = f1_2 * (int64_t) f5_2; + int64_t f1f6_2 = f1_2 * (int64_t) f6; + int64_t f1f7_4 = f1_2 * (int64_t) f7_2; + int64_t f1f8_2 = f1_2 * (int64_t) f8; + int64_t f1f9_76 = f1_2 * (int64_t) f9_38; + int64_t f2f2 = f2 * (int64_t) f2; + int64_t f2f3_2 = f2_2 * (int64_t) f3; + int64_t f2f4_2 = f2_2 * (int64_t) f4; + int64_t f2f5_2 = f2_2 * (int64_t) f5; + int64_t f2f6_2 = f2_2 * (int64_t) f6; + int64_t f2f7_2 = f2_2 * (int64_t) f7; + int64_t f2f8_38 = f2_2 * (int64_t) f8_19; + int64_t f2f9_38 = f2 * (int64_t) f9_38; + int64_t f3f3_2 = f3_2 * (int64_t) f3; + int64_t f3f4_2 = f3_2 * (int64_t) f4; + int64_t f3f5_4 = f3_2 * (int64_t) f5_2; + int64_t f3f6_2 = f3_2 * (int64_t) f6; + int64_t f3f7_76 = f3_2 * (int64_t) f7_38; + int64_t f3f8_38 = f3_2 * (int64_t) f8_19; + int64_t f3f9_76 = f3_2 * (int64_t) f9_38; + int64_t f4f4 = f4 * (int64_t) f4; + int64_t f4f5_2 = f4_2 * (int64_t) f5; + int64_t f4f6_38 = f4_2 * (int64_t) f6_19; + int64_t f4f7_38 = f4 * (int64_t) f7_38; + int64_t f4f8_38 = f4_2 * (int64_t) f8_19; + int64_t f4f9_38 = f4 * (int64_t) f9_38; + int64_t f5f5_38 = f5 * (int64_t) f5_38; + int64_t f5f6_38 = f5_2 * (int64_t) f6_19; + int64_t f5f7_76 = f5_2 * (int64_t) f7_38; + int64_t f5f8_38 = f5_2 * (int64_t) f8_19; + int64_t f5f9_76 = f5_2 * (int64_t) f9_38; + int64_t f6f6_19 = f6 * (int64_t) f6_19; + int64_t f6f7_38 = f6 * (int64_t) f7_38; + int64_t f6f8_38 = f6_2 * (int64_t) f8_19; + int64_t f6f9_38 = f6 * (int64_t) f9_38; + int64_t f7f7_38 = f7 * (int64_t) f7_38; + int64_t f7f8_38 = f7_2 * (int64_t) f8_19; + int64_t f7f9_76 = f7_2 * (int64_t) f9_38; + int64_t f8f8_19 = f8 * (int64_t) f8_19; + int64_t f8f9_38 = f8 * (int64_t) f9_38; + int64_t f9f9_38 = f9 * (int64_t) f9_38; + + int64_t h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38; + int64_t h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38; + int64_t h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19; + int64_t h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38; + int64_t h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38; + int64_t h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38; + int64_t h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19; + int64_t h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38; + int64_t h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38; + int64_t h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2; + + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + h0 += h0; + h1 += h1; + h2 += h2; + h3 += h3; + h4 += h4; + h5 += h5; + h6 += h6; + h7 += h7; + h8 += h8; + h9 += h9; + + carry0 = (h0 + (int64_t)(1L << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((uint64_t) 1L << 26); + carry4 = (h4 + (int64_t)(1L << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((uint64_t) 1L << 26); + + carry1 = (h1 + (int64_t)(1L << 24)) >> 25; + h2 += carry1; + h1 -= carry1 * ((uint64_t) 1L << 25); + carry5 = (h5 + (int64_t)(1L << 24)) >> 25; + h6 += carry5; + h5 -= carry5 * ((uint64_t) 1L << 25); + + carry2 = (h2 + (int64_t)(1L << 25)) >> 26; + h3 += carry2; + h2 -= carry2 * ((uint64_t) 1L << 26); + carry6 = (h6 + (int64_t)(1L << 25)) >> 26; + h7 += carry6; + h6 -= carry6 * ((uint64_t) 1L << 26); + + carry3 = (h3 + (int64_t)(1L << 24)) >> 25; + h4 += carry3; + h3 -= carry3 * ((uint64_t) 1L << 25); + carry7 = (h7 + (int64_t)(1L << 24)) >> 25; + h8 += carry7; + h7 -= carry7 * ((uint64_t) 1L << 25); + + carry4 = (h4 + (int64_t)(1L << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((uint64_t) 1L << 26); + carry8 = (h8 + (int64_t)(1L << 25)) >> 26; + h9 += carry8; + h8 -= carry8 * ((uint64_t) 1L << 26); + + carry9 = (h9 + (int64_t)(1L << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 * ((uint64_t) 1L << 25); + + carry0 = (h0 + (int64_t)(1L << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((uint64_t) 1L << 26); + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + +static inline void +fe25519_mul32(fe25519 h, const fe25519 f, uint32_t n) +{ + int64_t sn = (int64_t) n; + int32_t f0 = f[0]; + int32_t f1 = f[1]; + int32_t f2 = f[2]; + int32_t f3 = f[3]; + int32_t f4 = f[4]; + int32_t f5 = f[5]; + int32_t f6 = f[6]; + int32_t f7 = f[7]; + int32_t f8 = f[8]; + int32_t f9 = f[9]; + int64_t h0 = f0 * sn; + int64_t h1 = f1 * sn; + int64_t h2 = f2 * sn; + int64_t h3 = f3 * sn; + int64_t h4 = f4 * sn; + int64_t h5 = f5 * sn; + int64_t h6 = f6 * sn; + int64_t h7 = f7 * sn; + int64_t h8 = f8 * sn; + int64_t h9 = f9 * sn; + int64_t carry0, carry1, carry2, carry3, carry4, carry5, carry6, carry7, + carry8, carry9; + + carry9 = (h9 + ((int64_t) 1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 * ((int64_t) 1 << 25); + carry1 = (h1 + ((int64_t) 1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 * ((int64_t) 1 << 25); + carry3 = (h3 + ((int64_t) 1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 * ((int64_t) 1 << 25); + carry5 = (h5 + ((int64_t) 1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 * ((int64_t) 1 << 25); + carry7 = (h7 + ((int64_t) 1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 * ((int64_t) 1 << 25); + + carry0 = (h0 + ((int64_t) 1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((int64_t) 1 << 26); + carry2 = (h2 + ((int64_t) 1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 * ((int64_t) 1 << 26); + carry4 = (h4 + ((int64_t) 1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((int64_t) 1 << 26); + carry6 = (h6 + ((int64_t) 1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 * ((int64_t) 1 << 26); + carry8 = (h8 + ((int64_t) 1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 * ((int64_t) 1 << 26); + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} diff --git a/packages/runtime/vendor/libsodium/export.h b/packages/runtime/vendor/libsodium/export.h new file mode 100644 index 0000000000..a0074fc9cb --- /dev/null +++ b/packages/runtime/vendor/libsodium/export.h @@ -0,0 +1,57 @@ + +#ifndef sodium_export_H +#define sodium_export_H + +#include +#include +#include + +#if !defined(__clang__) && !defined(__GNUC__) +# ifdef __attribute__ +# undef __attribute__ +# endif +# define __attribute__(a) +#endif + +#ifdef SODIUM_STATIC +# define SODIUM_EXPORT +# define SODIUM_EXPORT_WEAK +#else +# if defined(_MSC_VER) +# ifdef SODIUM_DLL_EXPORT +# define SODIUM_EXPORT __declspec(dllexport) +# else +# define SODIUM_EXPORT __declspec(dllimport) +# endif +# else +# if defined(__SUNPRO_C) +# ifndef __GNU_C__ +# define SODIUM_EXPORT __attribute__ (visibility(__global)) +# else +# define SODIUM_EXPORT __attribute__ __global +# endif +# elif defined(_MSG_VER) +# define SODIUM_EXPORT extern __declspec(dllexport) +# else +# define SODIUM_EXPORT __attribute__ ((visibility ("default"))) +# endif +# endif +# if defined(__ELF__) && !defined(SODIUM_DISABLE_WEAK_FUNCTIONS) +# define SODIUM_EXPORT_WEAK SODIUM_EXPORT __attribute__((weak)) +# else +# define SODIUM_EXPORT_WEAK SODIUM_EXPORT +# endif +#endif + +#ifndef CRYPTO_ALIGN +# if defined(__INTEL_COMPILER) || defined(_MSC_VER) +# define CRYPTO_ALIGN(x) __declspec(align(x)) +# else +# define CRYPTO_ALIGN(x) __attribute__ ((aligned(x))) +# endif +#endif + +#define SODIUM_MIN(A, B) ((A) < (B) ? (A) : (B)) +#define SODIUM_SIZE_MAX SODIUM_MIN(UINT64_MAX, SIZE_MAX) + +#endif diff --git a/packages/runtime/vendor/libsodium/fe_25_5/base.h b/packages/runtime/vendor/libsodium/fe_25_5/base.h new file mode 100644 index 0000000000..e18530bbb1 --- /dev/null +++ b/packages/runtime/vendor/libsodium/fe_25_5/base.h @@ -0,0 +1,1344 @@ +{ /* 0/31 */ + { + { 25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605 }, + { -12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378 }, + { -8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546 } + }, + { + { -12815894, -12976347, -21581243, 11784320, -25355658, -2750717, -11717903, -3814571, -358445, -10211303 }, + { -21703237, 6903825, 27185491, 6451973, -29577724, -9554005, -15616551, 11189268, -26829678, -5319081 }, + { 26966642, 11152617, 32442495, 15396054, 14353839, -12752335, -3128826, -9541118, -15472047, -4166697 } + }, + { + { 15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024 }, + { 16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574 }, + { 30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357 } + }, + { + { -17036878, 13921892, 10945806, -6033431, 27105052, -16084379, -28926210, 15006023, 3284568, -6276540 }, + { 23599295, -8306047, -11193664, -7687416, 13236774, 10506355, 7464579, 9656445, 13059162, 10374397 }, + { 7798556, 16710257, 3033922, 2874086, 28997861, 2835604, 32406664, -3839045, -641708, -101325 } + }, + { + { 10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380 }, + { 4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306 }, + { 19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942 } + }, + { + { -15371964, -12862754, 32573250, 4720197, -26436522, 5875511, -19188627, -15224819, -9818940, -12085777 }, + { -8549212, 109983, 15149363, 2178705, 22900618, 4543417, 3044240, -15689887, 1762328, 14866737 }, + { -18199695, -15951423, -10473290, 1707278, -17185920, 3916101, -28236412, 3959421, 27914454, 4383652 } + }, + { + { 5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766 }, + { -30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701 }, + { 28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300 } + }, + { + { 14499471, -2729599, -33191113, -4254652, 28494862, 14271267, 30290735, 10876454, -33154098, 2381726 }, + { -7195431, -2655363, -14730155, 462251, -27724326, 3941372, -6236617, 3696005, -32300832, 15351955 }, + { 27431194, 8222322, 16448760, -3907995, -18707002, 11938355, -32961401, -2970515, 29551813, 10109425 } + } +}, +{ /* 1/31 */ + { + { -13657040, -13155431, -31283750, 11777098, 21447386, 6519384, -2378284, -1627556, 10092783, -4764171 }, + { 27939166, 14210322, 4677035, 16277044, -22964462, -12398139, -32508754, 12005538, -17810127, 12803510 }, + { 17228999, -15661624, -1233527, 300140, -1224870, -11714777, 30364213, -9038194, 18016357, 4397660 } + }, + { + { -10958843, -7690207, 4776341, -14954238, 27850028, -15602212, -26619106, 14544525, -17477504, 982639 }, + { 29253598, 15796703, -2863982, -9908884, 10057023, 3163536, 7332899, -4120128, -21047696, 9934963 }, + { 5793303, 16271923, -24131614, -10116404, 29188560, 1206517, -14747930, 4559895, -30123922, -10897950 } + }, + { + { -27643952, -11493006, 16282657, -11036493, 28414021, -15012264, 24191034, 4541697, -13338309, 5500568 }, + { 12650548, -1497113, 9052871, 11355358, -17680037, -8400164, -17430592, 12264343, 10874051, 13524335 }, + { 25556948, -3045990, 714651, 2510400, 23394682, -10415330, 33119038, 5080568, -22528059, 5376628 } + }, + { + { -26088264, -4011052, -17013699, -3537628, -6726793, 1920897, -22321305, -9447443, 4535768, 1569007 }, + { -2255422, 14606630, -21692440, -8039818, 28430649, 8775819, -30494562, 3044290, 31848280, 12543772 }, + { -22028579, 2943893, -31857513, 6777306, 13784462, -4292203, -27377195, -2062731, 7718482, 14474653 } + }, + { + { 2385315, 2454213, -22631320, 46603, -4437935, -15680415, 656965, -7236665, 24316168, -5253567 }, + { 13741529, 10911568, -33233417, -8603737, -20177830, -1033297, 33040651, -13424532, -20729456, 8321686 }, + { 21060490, -2212744, 15712757, -4336099, 1639040, 10656336, 23845965, -11874838, -9984458, 608372 } + }, + { + { -13672732, -15087586, -10889693, -7557059, -6036909, 11305547, 1123968, -6780577, 27229399, 23887 }, + { -23244140, -294205, -11744728, 14712571, -29465699, -2029617, 12797024, -6440308, -1633405, 16678954 }, + { -29500620, 4770662, -16054387, 14001338, 7830047, 9564805, -1508144, -4795045, -17169265, 4904953 } + }, + { + { 24059557, 14617003, 19037157, -15039908, 19766093, -14906429, 5169211, 16191880, 2128236, -4326833 }, + { -16981152, 4124966, -8540610, -10653797, 30336522, -14105247, -29806336, 916033, -6882542, -2986532 }, + { -22630907, 12419372, -7134229, -7473371, -16478904, 16739175, 285431, 2763829, 15736322, 4143876 } + }, + { + { 2379352, 11839345, -4110402, -5988665, 11274298, 794957, 212801, -14594663, 23527084, -16458268 }, + { 33431127, -11130478, -17838966, -15626900, 8909499, 8376530, -32625340, 4087881, -15188911, -14416214 }, + { 1767683, 7197987, -13205226, -2022635, -13091350, 448826, 5799055, 4357868, -4774191, -16323038 } + } +}, +{ /* 2/31 */ + { + { 6721966, 13833823, -23523388, -1551314, 26354293, -11863321, 23365147, -3949732, 7390890, 2759800 }, + { 4409041, 2052381, 23373853, 10530217, 7676779, -12885954, 21302353, -4264057, 1244380, -12919645 }, + { -4421239, 7169619, 4982368, -2957590, 30256825, -2777540, 14086413, 9208236, 15886429, 16489664 } + }, + { + { 1996075, 10375649, 14346367, 13311202, -6874135, -16438411, -13693198, 398369, -30606455, -712933 }, + { -25307465, 9795880, -2777414, 14878809, -33531835, 14780363, 13348553, 12076947, -30836462, 5113182 }, + { -17770784, 11797796, 31950843, 13929123, -25888302, 12288344, -30341101, -7336386, 13847711, 5387222 } + }, + { + { -18582163, -3416217, 17824843, -2340966, 22744343, -10442611, 8763061, 3617786, -19600662, 10370991 }, + { 20246567, -14369378, 22358229, -543712, 18507283, -10413996, 14554437, -8746092, 32232924, 16763880 }, + { 9648505, 10094563, 26416693, 14745928, -30374318, -6472621, 11094161, 15689506, 3140038, -16510092 } + }, + { + { -16160072, 5472695, 31895588, 4744994, 8823515, 10365685, -27224800, 9448613, -28774454, 366295 }, + { 19153450, 11523972, -11096490, -6503142, -24647631, 5420647, 28344573, 8041113, 719605, 11671788 }, + { 8678025, 2694440, -6808014, 2517372, 4964326, 11152271, -15432916, -15266516, 27000813, -10195553 } + }, + { + { -15157904, 7134312, 8639287, -2814877, -7235688, 10421742, 564065, 5336097, 6750977, -14521026 }, + { 11836410, -3979488, 26297894, 16080799, 23455045, 15735944, 1695823, -8819122, 8169720, 16220347 }, + { -18115838, 8653647, 17578566, -6092619, -8025777, -16012763, -11144307, -2627664, -5990708, -14166033 } + }, + { + { -23308498, -10968312, 15213228, -10081214, -30853605, -11050004, 27884329, 2847284, 2655861, 1738395 }, + { -27537433, -14253021, -25336301, -8002780, -9370762, 8129821, 21651608, -3239336, -19087449, -11005278 }, + { 1533110, 3437855, 23735889, 459276, 29970501, 11335377, 26030092, 5821408, 10478196, 8544890 } + }, + { + { 32173121, -16129311, 24896207, 3921497, 22579056, -3410854, 19270449, 12217473, 17789017, -3395995 }, + { -30552961, -2228401, -15578829, -10147201, 13243889, 517024, 15479401, -3853233, 30460520, 1052596 }, + { -11614875, 13323618, 32618793, 8175907, -15230173, 12596687, 27491595, -4612359, 3179268, -9478891 } + }, + { + { 31947069, -14366651, -4640583, -15339921, -15125977, -6039709, -14756777, -16411740, 19072640, -9511060 }, + { 11685058, 11822410, 3158003, -13952594, 33402194, -4165066, 5977896, -5215017, 473099, 5040608 }, + { -20290863, 8198642, -27410132, 11602123, 1290375, -2799760, 28326862, 1721092, -19558642, -3131606 } + } +}, +{ /* 3/31 */ + { + { 7881532, 10687937, 7578723, 7738378, -18951012, -2553952, 21820786, 8076149, -27868496, 11538389 }, + { -19935666, 3899861, 18283497, -6801568, -15728660, -11249211, 8754525, 7446702, -5676054, 5797016 }, + { -11295600, -3793569, -15782110, -7964573, 12708869, -8456199, 2014099, -9050574, -2369172, -5877341 } + }, + { + { -22472376, -11568741, -27682020, 1146375, 18956691, 16640559, 1192730, -3714199, 15123619, 10811505 }, + { 14352098, -3419715, -18942044, 10822655, 32750596, 4699007, -70363, 15776356, -28886779, -11974553 }, + { -28241164, -8072475, -4978962, -5315317, 29416931, 1847569, -20654173, -16484855, 4714547, -9600655 } + }, + { + { 15200332, 8368572, 19679101, 15970074, -31872674, 1959451, 24611599, -4543832, -11745876, 12340220 }, + { 12876937, -10480056, 33134381, 6590940, -6307776, 14872440, 9613953, 8241152, 15370987, 9608631 }, + { -4143277, -12014408, 8446281, -391603, 4407738, 13629032, -7724868, 15866074, -28210621, -8814099 } + }, + { + { 26660628, -15677655, 8393734, 358047, -7401291, 992988, -23904233, 858697, 20571223, 8420556 }, + { 14620715, 13067227, -15447274, 8264467, 14106269, 15080814, 33531827, 12516406, -21574435, -12476749 }, + { 236881, 10476226, 57258, -14677024, 6472998, 2466984, 17258519, 7256740, 8791136, 15069930 } + }, + { + { 1276410, -9371918, 22949635, -16322807, -23493039, -5702186, 14711875, 4874229, -30663140, -2331391 }, + { 5855666, 4990204, -13711848, 7294284, -7804282, 1924647, -1423175, -7912378, -33069337, 9234253 }, + { 20590503, -9018988, 31529744, -7352666, -2706834, 10650548, 31559055, -11609587, 18979186, 13396066 } + }, + { + { 24474287, 4968103, 22267082, 4407354, 24063882, -8325180, -18816887, 13594782, 33514650, 7021958 }, + { -11566906, -6565505, -21365085, 15928892, -26158305, 4315421, -25948728, -3916677, -21480480, 12868082 }, + { -28635013, 13504661, 19988037, -2132761, 21078225, 6443208, -21446107, 2244500, -12455797, -8089383 } + }, + { + { -30595528, 13793479, -5852820, 319136, -25723172, -6263899, 33086546, 8957937, -15233648, 5540521 }, + { -11630176, -11503902, -8119500, -7643073, 2620056, 1022908, -23710744, -1568984, -16128528, -14962807 }, + { 23152971, 775386, 27395463, 14006635, -9701118, 4649512, 1689819, 892185, -11513277, -15205948 } + }, + { + { 9770129, 9586738, 26496094, 4324120, 1556511, -3550024, 27453819, 4763127, -19179614, 5867134 }, + { -32765025, 1927590, 31726409, -4753295, 23962434, -16019500, 27846559, 5931263, -29749703, -16108455 }, + { 27461885, -2977536, 22380810, 1815854, -23033753, -3031938, 7283490, -15148073, -19526700, 7734629 } + } +}, +{ /* 4/31 */ + { + { -8010264, -9590817, -11120403, 6196038, 29344158, -13430885, 7585295, -3176626, 18549497, 15302069 }, + { -32658337, -6171222, -7672793, -11051681, 6258878, 13504381, 10458790, -6418461, -8872242, 8424746 }, + { 24687205, 8613276, -30667046, -3233545, 1863892, -1830544, 19206234, 7134917, -11284482, -828919 } + }, + { + { 11334899, -9218022, 8025293, 12707519, 17523892, -10476071, 10243738, -14685461, -5066034, 16498837 }, + { 8911542, 6887158, -9584260, -6958590, 11145641, -9543680, 17303925, -14124238, 6536641, 10543906 }, + { -28946384, 15479763, -17466835, 568876, -1497683, 11223454, -2669190, -16625574, -27235709, 8876771 } + }, + { + { -25742899, -12566864, -15649966, -846607, -33026686, -796288, -33481822, 15824474, -604426, -9039817 }, + { 10330056, 70051, 7957388, -9002667, 9764902, 15609756, 27698697, -4890037, 1657394, 3084098 }, + { 10477963, -7470260, 12119566, -13250805, 29016247, -5365589, 31280319, 14396151, -30233575, 15272409 } + }, + { + { -12288309, 3169463, 28813183, 16658753, 25116432, -5630466, -25173957, -12636138, -25014757, 1950504 }, + { -26180358, 9489187, 11053416, -14746161, -31053720, 5825630, -8384306, -8767532, 15341279, 8373727 }, + { 28685821, 7759505, -14378516, -12002860, -31971820, 4079242, 298136, -10232602, -2878207, 15190420 } + }, + { + { -32932876, 13806336, -14337485, -15794431, -24004620, 10940928, 8669718, 2742393, -26033313, -6875003 }, + { -1580388, -11729417, -25979658, -11445023, -17411874, -10912854, 9291594, -16247779, -12154742, 6048605 }, + { -30305315, 14843444, 1539301, 11864366, 20201677, 1900163, 13934231, 5128323, 11213262, 9168384 } + }, + { + { -26280513, 11007847, 19408960, -940758, -18592965, -4328580, -5088060, -11105150, 20470157, -16398701 }, + { -23136053, 9282192, 14855179, -15390078, -7362815, -14408560, -22783952, 14461608, 14042978, 5230683 }, + { 29969567, -2741594, -16711867, -8552442, 9175486, -2468974, 21556951, 3506042, -5933891, -12449708 } + }, + { + { -3144746, 8744661, 19704003, 4581278, -20430686, 6830683, -21284170, 8971513, -28539189, 15326563 }, + { -19464629, 10110288, -17262528, -3503892, -23500387, 1355669, -15523050, 15300988, -20514118, 9168260 }, + { -5353335, 4488613, -23803248, 16314347, 7780487, -15638939, -28948358, 9601605, 33087103, -9011387 } + }, + { + { -19443170, -15512900, -20797467, -12445323, -29824447, 10229461, -27444329, -15000531, -5996870, 15664672 }, + { 23294591, -16632613, -22650781, -8470978, 27844204, 11461195, 13099750, -2460356, 18151676, 13417686 }, + { -24722913, -4176517, -31150679, 5988919, -26858785, 6685065, 1661597, -12551441, 15271676, -15452665 } + } +}, +{ /* 5/31 */ + { + { 11433042, -13228665, 8239631, -5279517, -1985436, -725718, -18698764, 2167544, -6921301, -13440182 }, + { -31436171, 15575146, 30436815, 12192228, -22463353, 9395379, -9917708, -8638997, 12215110, 12028277 }, + { 14098400, 6555944, 23007258, 5757252, -15427832, -12950502, 30123440, 4617780, -16900089, -655628 } + }, + { + { -4026201, -15240835, 11893168, 13718664, -14809462, 1847385, -15819999, 10154009, 23973261, -12684474 }, + { -26531820, -3695990, -1908898, 2534301, -31870557, -16550355, 18341390, -11419951, 32013174, -10103539 }, + { -25479301, 10876443, -11771086, -14625140, -12369567, 1838104, 21911214, 6354752, 4425632, -837822 } + }, + { + { -10433389, -14612966, 22229858, -3091047, -13191166, 776729, -17415375, -12020462, 4725005, 14044970 }, + { 19268650, -7304421, 1555349, 8692754, -21474059, -9910664, 6347390, -1411784, -19522291, -16109756 }, + { -24864089, 12986008, -10898878, -5558584, -11312371, -148526, 19541418, 8180106, 9282262, 10282508 } + }, + { + { -26205082, 4428547, -8661196, -13194263, 4098402, -14165257, 15522535, 8372215, 5542595, -10702683 }, + { -10562541, 14895633, 26814552, -16673850, -17480754, -2489360, -2781891, 6993761, -18093885, 10114655 }, + { -20107055, -929418, 31422704, 10427861, -7110749, 6150669, -29091755, -11529146, 25953725, -106158 } + }, + { + { -4234397, -8039292, -9119125, 3046000, 2101609, -12607294, 19390020, 6094296, -3315279, 12831125 }, + { -15998678, 7578152, 5310217, 14408357, -33548620, -224739, 31575954, 6326196, 7381791, -2421839 }, + { -20902779, 3296811, 24736065, -16328389, 18374254, 7318640, 6295303, 8082724, -15362489, 12339664 } + }, + { + { 27724736, 2291157, 6088201, -14184798, 1792727, 5857634, 13848414, 15768922, 25091167, 14856294 }, + { -18866652, 8331043, 24373479, 8541013, -701998, -9269457, 12927300, -12695493, -22182473, -9012899 }, + { -11423429, -5421590, 11632845, 3405020, 30536730, -11674039, -27260765, 13866390, 30146206, 9142070 } + }, + { + { 3924129, -15307516, -13817122, -10054960, 12291820, -668366, -27702774, 9326384, -8237858, 4171294 }, + { -15921940, 16037937, 6713787, 16606682, -21612135, 2790944, 26396185, 3731949, 345228, -5462949 }, + { -21327538, 13448259, 25284571, 1143661, 20614966, -8849387, 2031539, -12391231, -16253183, -13582083 } + }, + { + { 31016211, -16722429, 26371392, -14451233, -5027349, 14854137, 17477601, 3842657, 28012650, -16405420 }, + { -5075835, 9368966, -8562079, -4600902, -15249953, 6970560, -9189873, 16292057, -8867157, 3507940 }, + { 29439664, 3537914, 23333589, 6997794, -17555561, -11018068, -15209202, -15051267, -9164929, 6580396 } + } +}, +{ /* 6/31 */ + { + { -12185861, -7679788, 16438269, 10826160, -8696817, -6235611, 17860444, -9273846, -2095802, 9304567 }, + { 20714564, -4336911, 29088195, 7406487, 11426967, -5095705, 14792667, -14608617, 5289421, -477127 }, + { -16665533, -10650790, -6160345, -13305760, 9192020, -1802462, 17271490, 12349094, 26939669, -3752294 } + }, + { + { -12889898, 9373458, 31595848, 16374215, 21471720, 13221525, -27283495, -12348559, -3698806, 117887 }, + { 22263325, -6560050, 3984570, -11174646, -15114008, -566785, 28311253, 5358056, -23319780, 541964 }, + { 16259219, 3261970, 2309254, -15534474, -16885711, -4581916, 24134070, -16705829, -13337066, -13552195 } + }, + { + { 9378160, -13140186, -22845982, -12745264, 28198281, -7244098, -2399684, -717351, 690426, 14876244 }, + { 24977353, -314384, -8223969, -13465086, 28432343, -1176353, -13068804, -12297348, -22380984, 6618999 }, + { -1538174, 11685646, 12944378, 13682314, -24389511, -14413193, 8044829, -13817328, 32239829, -5652762 } + }, + { + { -18603066, 4762990, -926250, 8885304, -28412480, -3187315, 9781647, -10350059, 32779359, 5095274 }, + { -33008130, -5214506, -32264887, -3685216, 9460461, -9327423, -24601656, 14506724, 21639561, -2630236 }, + { -16400943, -13112215, 25239338, 15531969, 3987758, -4499318, -1289502, -6863535, 17874574, 558605 } + }, + { + { -13600129, 10240081, 9171883, 16131053, -20869254, 9599700, 33499487, 5080151, 2085892, 5119761 }, + { -22205145, -2519528, -16381601, 414691, -25019550, 2170430, 30634760, -8363614, -31999993, -5759884 }, + { -6845704, 15791202, 8550074, -1312654, 29928809, -12092256, 27534430, -7192145, -22351378, 12961482 } + }, + { + { -24492060, -9570771, 10368194, 11582341, -23397293, -2245287, 16533930, 8206996, -30194652, -5159638 }, + { -11121496, -3382234, 2307366, 6362031, -135455, 8868177, -16835630, 7031275, 7589640, 8945490 }, + { -32152748, 8917967, 6661220, -11677616, -1192060, -15793393, 7251489, -11182180, 24099109, -14456170 } + }, + { + { 5019558, -7907470, 4244127, -14714356, -26933272, 6453165, -19118182, -13289025, -6231896, -10280736 }, + { 10853594, 10721687, 26480089, 5861829, -22995819, 1972175, -1866647, -10557898, -3363451, -6441124 }, + { -17002408, 5906790, 221599, -6563147, 7828208, -13248918, 24362661, -2008168, -13866408, 7421392 } + }, + { + { 8139927, -6546497, 32257646, -5890546, 30375719, 1886181, -21175108, 15441252, 28826358, -4123029 }, + { 6267086, 9695052, 7709135, -16603597, -32869068, -1886135, 14795160, -7840124, 13746021, -1742048 }, + { 28584902, 7787108, -6732942, -15050729, 22846041, -7571236, -3181936, -363524, 4771362, -8419958 } + } +}, +{ /* 7/31 */ + { + { 24949256, 6376279, -27466481, -8174608, -18646154, -9930606, 33543569, -12141695, 3569627, 11342593 }, + { 26514989, 4740088, 27912651, 3697550, 19331575, -11472339, 6809886, 4608608, 7325975, -14801071 }, + { -11618399, -14554430, -24321212, 7655128, -1369274, 5214312, -27400540, 10258390, -17646694, -8186692 } + }, + { + { 11431204, 15823007, 26570245, 14329124, 18029990, 4796082, -31446179, 15580664, 9280358, -3973687 }, + { -160783, -10326257, -22855316, -4304997, -20861367, -13621002, -32810901, -11181622, -15545091, 4387441 }, + { -20799378, 12194512, 3937617, -5805892, -27154820, 9340370, -24513992, 8548137, 20617071, -7482001 } + }, + { + { -938825, -3930586, -8714311, 16124718, 24603125, -6225393, -13775352, -11875822, 24345683, 10325460 }, + { -19855277, -1568885, -22202708, 8714034, 14007766, 6928528, 16318175, -1010689, 4766743, 3552007 }, + { -21751364, -16730916, 1351763, -803421, -4009670, 3950935, 3217514, 14481909, 10988822, -3994762 } + }, + { + { 15564307, -14311570, 3101243, 5684148, 30446780, -8051356, 12677127, -6505343, -8295852, 13296005 }, + { -9442290, 6624296, -30298964, -11913677, -4670981, -2057379, 31521204, 9614054, -30000824, 12074674 }, + { 4771191, -135239, 14290749, -13089852, 27992298, 14998318, -1413936, -1556716, 29832613, -16391035 } + }, + { + { 7064884, -7541174, -19161962, -5067537, -18891269, -2912736, 25825242, 5293297, -27122660, 13101590 }, + { -2298563, 2439670, -7466610, 1719965, -27267541, -16328445, 32512469, -5317593, -30356070, -4190957 }, + { -30006540, 10162316, -33180176, 3981723, -16482138, -13070044, 14413974, 9515896, 19568978, 9628812 } + }, + { + { 33053803, 199357, 15894591, 1583059, 27380243, -4580435, -17838894, -6106839, -6291786, 3437740 }, + { -18978877, 3884493, 19469877, 12726490, 15913552, 13614290, -22961733, 70104, 7463304, 4176122 }, + { -27124001, 10659917, 11482427, -16070381, 12771467, -6635117, -32719404, -5322751, 24216882, 5944158 } + }, + { + { 8894125, 7450974, -2664149, -9765752, -28080517, -12389115, 19345746, 14680796, 11632993, 5847885 }, + { 26942781, -2315317, 9129564, -4906607, 26024105, 11769399, -11518837, 6367194, -9727230, 4782140 }, + { 19916461, -4828410, -22910704, -11414391, 25606324, -5972441, 33253853, 8220911, 6358847, -1873857 } + }, + { + { 801428, -2081702, 16569428, 11065167, 29875704, 96627, 7908388, -4480480, -13538503, 1387155 }, + { 19646058, 5720633, -11416706, 12814209, 11607948, 12749789, 14147075, 15156355, -21866831, 11835260 }, + { 19299512, 1155910, 28703737, 14890794, 2925026, 7269399, 26121523, 15467869, -26560550, 5052483 } + } +}, +{ /* 8/31 */ + { + { -3017432, 10058206, 1980837, 3964243, 22160966, 12322533, -6431123, -12618185, 12228557, -7003677 }, + { 32944382, 14922211, -22844894, 5188528, 21913450, -8719943, 4001465, 13238564, -6114803, 8653815 }, + { 22865569, -4652735, 27603668, -12545395, 14348958, 8234005, 24808405, 5719875, 28483275, 2841751 } + }, + { + { -16420968, -1113305, -327719, -12107856, 21886282, -15552774, -1887966, -315658, 19932058, -12739203 }, + { -11656086, 10087521, -8864888, -5536143, -19278573, -3055912, 3999228, 13239134, -4777469, -13910208 }, + { 1382174, -11694719, 17266790, 9194690, -13324356, 9720081, 20403944, 11284705, -14013818, 3093230 } + }, + { + { 16650921, -11037932, -1064178, 1570629, -8329746, 7352753, -302424, 16271225, -24049421, -6691850 }, + { -21911077, -5927941, -4611316, -5560156, -31744103, -10785293, 24123614, 15193618, -21652117, -16739389 }, + { -9935934, -4289447, -25279823, 4372842, 2087473, 10399484, 31870908, 14690798, 17361620, 11864968 } + }, + { + { -11307610, 6210372, 13206574, 5806320, -29017692, -13967200, -12331205, -7486601, -25578460, -16240689 }, + { 14668462, -12270235, 26039039, 15305210, 25515617, 4542480, 10453892, 6577524, 9145645, -6443880 }, + { 5974874, 3053895, -9433049, -10385191, -31865124, 3225009, -7972642, 3936128, -5652273, -3050304 } + }, + { + { 30625386, -4729400, -25555961, -12792866, -20484575, 7695099, 17097188, -16303496, -27999779, 1803632 }, + { -3553091, 9865099, -5228566, 4272701, -5673832, -16689700, 14911344, 12196514, -21405489, 7047412 }, + { 20093277, 9920966, -11138194, -5343857, 13161587, 12044805, -32856851, 4124601, -32343828, -10257566 } + }, + { + { -20788824, 14084654, -13531713, 7842147, 19119038, -13822605, 4752377, -8714640, -21679658, 2288038 }, + { -26819236, -3283715, 29965059, 3039786, -14473765, 2540457, 29457502, 14625692, -24819617, 12570232 }, + { -1063558, -11551823, 16920318, 12494842, 1278292, -5869109, -21159943, -3498680, -11974704, 4724943 } + }, + { + { 17960970, -11775534, -4140968, -9702530, -8876562, -1410617, -12907383, -8659932, -29576300, 1903856 }, + { 23134274, -14279132, -10681997, -1611936, 20684485, 15770816, -12989750, 3190296, 26955097, 14109738 }, + { 15308788, 5320727, -30113809, -14318877, 22902008, 7767164, 29425325, -11277562, 31960942, 11934971 } + }, + { + { -27395711, 8435796, 4109644, 12222639, -24627868, 14818669, 20638173, 4875028, 10491392, 1379718 }, + { -13159415, 9197841, 3875503, -8936108, -1383712, -5879801, 33518459, 16176658, 21432314, 12180697 }, + { -11787308, 11500838, 13787581, -13832590, -22430679, 10140205, 1465425, 12689540, -10301319, -13872883 } + } +}, +{ /* 9/31 */ + { + { 5414091, -15386041, -21007664, 9643570, 12834970, 1186149, -2622916, -1342231, 26128231, 6032912 }, + { -26337395, -13766162, 32496025, -13653919, 17847801, -12669156, 3604025, 8316894, -25875034, -10437358 }, + { 3296484, 6223048, 24680646, -12246460, -23052020, 5903205, -8862297, -4639164, 12376617, 3188849 } + }, + { + { 29190488, -14659046, 27549113, -1183516, 3520066, -10697301, 32049515, -7309113, -16109234, -9852307 }, + { -14744486, -9309156, 735818, -598978, -20407687, -5057904, 25246078, -15795669, 18640741, -960977 }, + { -6928835, -16430795, 10361374, 5642961, 4910474, 12345252, -31638386, -494430, 10530747, 1053335 } + }, + { + { -29265967, -14186805, -13538216, -12117373, -19457059, -10655384, -31462369, -2948985, 24018831, 15026644 }, + { -22592535, -3145277, -2289276, 5953843, -13440189, 9425631, 25310643, 13003497, -2314791, -15145616 }, + { -27419985, -603321, -8043984, -1669117, -26092265, 13987819, -27297622, 187899, -23166419, -2531735 } + }, + { + { -21744398, -13810475, 1844840, 5021428, -10434399, -15911473, 9716667, 16266922, -5070217, 726099 }, + { 29370922, -6053998, 7334071, -15342259, 9385287, 2247707, -13661962, -4839461, 30007388, -15823341 }, + { -936379, 16086691, 23751945, -543318, -1167538, -5189036, 9137109, 730663, 9835848, 4555336 } + }, + { + { -23376435, 1410446, -22253753, -12899614, 30867635, 15826977, 17693930, 544696, -11985298, 12422646 }, + { 31117226, -12215734, -13502838, 6561947, -9876867, -12757670, -5118685, -4096706, 29120153, 13924425 }, + { -17400879, -14233209, 19675799, -2734756, -11006962, -5858820, -9383939, -11317700, 7240931, -237388 } + }, + { + { -31361739, -11346780, -15007447, -5856218, -22453340, -12152771, 1222336, 4389483, 3293637, -15551743 }, + { -16684801, -14444245, 11038544, 11054958, -13801175, -3338533, -24319580, 7733547, 12796905, -6335822 }, + { -8759414, -10817836, -25418864, 10783769, -30615557, -9746811, -28253339, 3647836, 3222231, -11160462 } + }, + { + { 18606113, 1693100, -25448386, -15170272, 4112353, 10045021, 23603893, -2048234, -7550776, 2484985 }, + { 9255317, -3131197, -12156162, -1004256, 13098013, -9214866, 16377220, -2102812, -19802075, -3034702 }, + { -22729289, 7496160, -5742199, 11329249, 19991973, -3347502, -31718148, 9936966, -30097688, -10618797 } + }, + { + { 21878590, -5001297, 4338336, 13643897, -3036865, 13160960, 19708896, 5415497, -7360503, -4109293 }, + { 27736861, 10103576, 12500508, 8502413, -3413016, -9633558, 10436918, -1550276, -23659143, -8132100 }, + { 19492550, -12104365, -29681976, -852630, -3208171, 12403437, 30066266, 8367329, 13243957, 8709688 } + } +}, +{ /* 10/31 */ + { + { 12015105, 2801261, 28198131, 10151021, 24818120, -4743133, -11194191, -5645734, 5150968, 7274186 }, + { 2831366, -12492146, 1478975, 6122054, 23825128, -12733586, 31097299, 6083058, 31021603, -9793610 }, + { -2529932, -2229646, 445613, 10720828, -13849527, -11505937, -23507731, 16354465, 15067285, -14147707 } + }, + { + { 7840942, 14037873, -33364863, 15934016, -728213, -3642706, 21403988, 1057586, -19379462, -12403220 }, + { 915865, -16469274, 15608285, -8789130, -24357026, 6060030, -17371319, 8410997, -7220461, 16527025 }, + { 32922597, -556987, 20336074, -16184568, 10903705, -5384487, 16957574, 52992, 23834301, 6588044 } + }, + { + { 32752030, 11232950, 3381995, -8714866, 22652988, -10744103, 17159699, 16689107, -20314580, -1305992 }, + { -4689649, 9166776, -25710296, -10847306, 11576752, 12733943, 7924251, -2752281, 1976123, -7249027 }, + { 21251222, 16309901, -2983015, -6783122, 30810597, 12967303, 156041, -3371252, 12331345, -8237197 } + }, + { + { 8651614, -4477032, -16085636, -4996994, 13002507, 2950805, 29054427, -5106970, 10008136, -4667901 }, + { 31486080, 15114593, -14261250, 12951354, 14369431, -7387845, 16347321, -13662089, 8684155, -10532952 }, + { 19443825, 11385320, 24468943, -9659068, -23919258, 2187569, -26263207, -6086921, 31316348, 14219878 } + }, + { + { -28594490, 1193785, 32245219, 11392485, 31092169, 15722801, 27146014, 6992409, 29126555, 9207390 }, + { 32382935, 1110093, 18477781, 11028262, -27411763, -7548111, -4980517, 10843782, -7957600, -14435730 }, + { 2814918, 7836403, 27519878, -7868156, -20894015, -11553689, -21494559, 8550130, 28346258, 1994730 } + }, + { + { -19578299, 8085545, -14000519, -3948622, 2785838, -16231307, -19516951, 7174894, 22628102, 8115180 }, + { -30405132, 955511, -11133838, -15078069, -32447087, -13278079, -25651578, 3317160, -9943017, 930272 }, + { -15303681, -6833769, 28856490, 1357446, 23421993, 1057177, 24091212, -1388970, -22765376, -10650715 } + }, + { + { -22751231, -5303997, -12907607, -12768866, -15811511, -7797053, -14839018, -16554220, -1867018, 8398970 }, + { -31969310, 2106403, -4736360, 1362501, 12813763, 16200670, 22981545, -6291273, 18009408, -15772772 }, + { -17220923, -9545221, -27784654, 14166835, 29815394, 7444469, 29551787, -3727419, 19288549, 1325865 } + }, + { + { 15100157, -15835752, -23923978, -1005098, -26450192, 15509408, 12376730, -3479146, 33166107, -8042750 }, + { 20909231, 13023121, -9209752, 16251778, -5778415, -8094914, 12412151, 10018715, 2213263, -13878373 }, + { 32529814, -11074689, 30361439, -16689753, -9135940, 1513226, 22922121, 6382134, -5766928, 8371348 } + } +}, +{ /* 11/31 */ + { + { 9923462, 11271500, 12616794, 3544722, -29998368, -1721626, 12891687, -8193132, -26442943, 10486144 }, + { -22597207, -7012665, 8587003, -8257861, 4084309, -12970062, 361726, 2610596, -23921530, -11455195 }, + { 5408411, -1136691, -4969122, 10561668, 24145918, 14240566, 31319731, -4235541, 19985175, -3436086 } + }, + { + { -13994457, 16616821, 14549246, 3341099, 32155958, 13648976, -17577068, 8849297, 65030, 8370684 }, + { -8320926, -12049626, 31204563, 5839400, -20627288, -1057277, -19442942, 6922164, 12743482, -9800518 }, + { -2361371, 12678785, 28815050, 4759974, -23893047, 4884717, 23783145, 11038569, 18800704, 255233 } + }, + { + { -5269658, -1773886, 13957886, 7990715, 23132995, 728773, 13393847, 9066957, 19258688, -14753793 }, + { -2936654, -10827535, -10432089, 14516793, -3640786, 4372541, -31934921, 2209390, -1524053, 2055794 }, + { 580882, 16705327, 5468415, -2683018, -30926419, -14696000, -7203346, -8994389, -30021019, 7394435 } + }, + { + { 23838809, 1822728, -15738443, 15242727, 8318092, -3733104, -21672180, -3492205, -4821741, 14799921 }, + { 13345610, 9759151, 3371034, -16137791, 16353039, 8577942, 31129804, 13496856, -9056018, 7402518 }, + { 2286874, -4435931, -20042458, -2008336, -13696227, 5038122, 11006906, -15760352, 8205061, 1607563 } + }, + { + { 14414086, -8002132, 3331830, -3208217, 22249151, -5594188, 18364661, -2906958, 30019587, -9029278 }, + { -27688051, 1585953, -10775053, 931069, -29120221, -11002319, -14410829, 12029093, 9944378, 8024 }, + { 4368715, -3709630, 29874200, -15022983, -20230386, -11410704, -16114594, -999085, -8142388, 5640030 } + }, + { + { 10299610, 13746483, 11661824, 16234854, 7630238, 5998374, 9809887, -16694564, 15219798, -14327783 }, + { 27425505, -5719081, 3055006, 10660664, 23458024, 595578, -15398605, -1173195, -18342183, 9742717 }, + { 6744077, 2427284, 26042789, 2720740, -847906, 1118974, 32324614, 7406442, 12420155, 1994844 } + }, + { + { 14012521, -5024720, -18384453, -9578469, -26485342, -3936439, -13033478, -10909803, 24319929, -6446333 }, + { 16412690, -4507367, 10772641, 15929391, -17068788, -4658621, 10555945, -10484049, -30102368, -4739048 }, + { 22397382, -7767684, -9293161, -12792868, 17166287, -9755136, -27333065, 6199366, 21880021, -12250760 } + }, + { + { -4283307, 5368523, -31117018, 8163389, -30323063, 3209128, 16557151, 8890729, 8840445, 4957760 }, + { -15447727, 709327, -6919446, -10870178, -29777922, 6522332, -21720181, 12130072, -14796503, 5005757 }, + { -2114751, -14308128, 23019042, 15765735, -25269683, 6002752, 10183197, -13239326, -16395286, -2176112 } + } +}, +{ /* 12/31 */ + { + { -19025756, 1632005, 13466291, -7995100, -23640451, 16573537, -32013908, -3057104, 22208662, 2000468 }, + { 3065073, -1412761, -25598674, -361432, -17683065, -5703415, -8164212, 11248527, -3691214, -7414184 }, + { 10379208, -6045554, 8877319, 1473647, -29291284, -12507580, 16690915, 2553332, -3132688, 16400289 } + }, + { + { 15716668, 1254266, -18472690, 7446274, -8448918, 6344164, -22097271, -7285580, 26894937, 9132066 }, + { 24158887, 12938817, 11085297, -8177598, -28063478, -4457083, -30576463, 64452, -6817084, -2692882 }, + { 13488534, 7794716, 22236231, 5989356, 25426474, -12578208, 2350710, -3418511, -4688006, 2364226 } + }, + { + { 16335052, 9132434, 25640582, 6678888, 1725628, 8517937, -11807024, -11697457, 15445875, -7798101 }, + { 29004207, -7867081, 28661402, -640412, -12794003, -7943086, 31863255, -4135540, -278050, -15759279 }, + { -6122061, -14866665, -28614905, 14569919, -10857999, -3591829, 10343412, -6976290, -29828287, -10815811 } + }, + { + { 27081650, 3463984, 14099042, -4517604, 1616303, -6205604, 29542636, 15372179, 17293797, 960709 }, + { 20263915, 11434237, -5765435, 11236810, 13505955, -10857102, -16111345, 6493122, -19384511, 7639714 }, + { -2830798, -14839232, 25403038, -8215196, -8317012, -16173699, 18006287, -16043750, 29994677, -15808121 } + }, + { + { 9769828, 5202651, -24157398, -13631392, -28051003, -11561624, -24613141, -13860782, -31184575, 709464 }, + { 12286395, 13076066, -21775189, -1176622, -25003198, 4057652, -32018128, -8890874, 16102007, 13205847 }, + { 13733362, 5599946, 10557076, 3195751, -5557991, 8536970, -25540170, 8525972, 10151379, 10394400 } + }, + { + { 4024660, -16137551, 22436262, 12276534, -9099015, -2686099, 19698229, 11743039, -33302334, 8934414 }, + { -15879800, -4525240, -8580747, -2934061, 14634845, -698278, -9449077, 3137094, -11536886, 11721158 }, + { 17555939, -5013938, 8268606, 2331751, -22738815, 9761013, 9319229, 8835153, -9205489, -1280045 } + }, + { + { -461409, -7830014, 20614118, 16688288, -7514766, -4807119, 22300304, 505429, 6108462, -6183415 }, + { -5070281, 12367917, -30663534, 3234473, 32617080, -8422642, 29880583, -13483331, -26898490, -7867459 }, + { -31975283, 5726539, 26934134, 10237677, -3173717, -605053, 24199304, 3795095, 7592688, -14992079 } + }, + { + { 21594432, -14964228, 17466408, -4077222, 32537084, 2739898, 6407723, 12018833, -28256052, 4298412 }, + { -20650503, -11961496, -27236275, 570498, 3767144, -1717540, 13891942, -1569194, 13717174, 10805743 }, + { -14676630, -15644296, 15287174, 11927123, 24177847, -8175568, -796431, 14860609, -26938930, -5863836 } + } +}, +{ /* 13/31 */ + { + { 12962541, 5311799, -10060768, 11658280, 18855286, -7954201, 13286263, -12808704, -4381056, 9882022 }, + { 18512079, 11319350, -20123124, 15090309, 18818594, 5271736, -22727904, 3666879, -23967430, -3299429 }, + { -6789020, -3146043, 16192429, 13241070, 15898607, -14206114, -10084880, -6661110, -2403099, 5276065 } + }, + { + { 30169808, -5317648, 26306206, -11750859, 27814964, 7069267, 7152851, 3684982, 1449224, 13082861 }, + { 10342826, 3098505, 2119311, 193222, 25702612, 12233820, 23697382, 15056736, -21016438, -8202000 }, + { -33150110, 3261608, 22745853, 7948688, 19370557, -15177665, -26171976, 6482814, -10300080, -11060101 } + }, + { + { 32869458, -5408545, 25609743, 15678670, -10687769, -15471071, 26112421, 2521008, -22664288, 6904815 }, + { 29506923, 4457497, 3377935, -9796444, -30510046, 12935080, 1561737, 3841096, -29003639, -6657642 }, + { 10340844, -6630377, -18656632, -2278430, 12621151, -13339055, 30878497, -11824370, -25584551, 5181966 } + }, + { + { 25940115, -12658025, 17324188, -10307374, -8671468, 15029094, 24396252, -16450922, -2322852, -12388574 }, + { -21765684, 9916823, -1300409, 4079498, -1028346, 11909559, 1782390, 12641087, 20603771, -6561742 }, + { -18882287, -11673380, 24849422, 11501709, 13161720, -4768874, 1925523, 11914390, 4662781, 7820689 } + }, + { + { 12241050, -425982, 8132691, 9393934, 32846760, -1599620, 29749456, 12172924, 16136752, 15264020 }, + { -10349955, -14680563, -8211979, 2330220, -17662549, -14545780, 10658213, 6671822, 19012087, 3772772 }, + { 3753511, -3421066, 10617074, 2028709, 14841030, -6721664, 28718732, -15762884, 20527771, 12988982 } + }, + { + { -14822485, -5797269, -3707987, 12689773, -898983, -10914866, -24183046, -10564943, 3299665, -12424953 }, + { -16777703, -15253301, -9642417, 4978983, 3308785, 8755439, 6943197, 6461331, -25583147, 8991218 }, + { -17226263, 1816362, -1673288, -6086439, 31783888, -8175991, -32948145, 7417950, -30242287, 1507265 } + }, + { + { 29692663, 6829891, -10498800, 4334896, 20945975, -11906496, -28887608, 8209391, 14606362, -10647073 }, + { -3481570, 8707081, 32188102, 5672294, 22096700, 1711240, -33020695, 9761487, 4170404, -2085325 }, + { -11587470, 14855945, -4127778, -1531857, -26649089, 15084046, 22186522, 16002000, -14276837, -8400798 } + }, + { + { -4811456, 13761029, -31703877, -2483919, -3312471, 7869047, -7113572, -9620092, 13240845, 10965870 }, + { -7742563, -8256762, -14768334, -13656260, -23232383, 12387166, 4498947, 14147411, 29514390, 4302863 }, + { -13413405, -12407859, 20757302, -13801832, 14785143, 8976368, -5061276, -2144373, 17846988, -13971927 } + } +}, +{ /* 14/31 */ + { + { -2244452, -754728, -4597030, -1066309, -6247172, 1455299, -21647728, -9214789, -5222701, 12650267 }, + { -9906797, -16070310, 21134160, 12198166, -27064575, 708126, 387813, 13770293, -19134326, 10958663 }, + { 22470984, 12369526, 23446014, -5441109, -21520802, -9698723, -11772496, -11574455, -25083830, 4271862 } + }, + { + { -25169565, -10053642, -19909332, 15361595, -5984358, 2159192, 75375, -4278529, -32526221, 8469673 }, + { 15854970, 4148314, -8893890, 7259002, 11666551, 13824734, -30531198, 2697372, 24154791, -9460943 }, + { 15446137, -15806644, 29759747, 14019369, 30811221, -9610191, -31582008, 12840104, 24913809, 9815020 } + }, + { + { -4709286, -5614269, -31841498, -12288893, -14443537, 10799414, -9103676, 13438769, 18735128, 9466238 }, + { 11933045, 9281483, 5081055, -5183824, -2628162, -4905629, -7727821, -10896103, -22728655, 16199064 }, + { 14576810, 379472, -26786533, -8317236, -29426508, -10812974, -102766, 1876699, 30801119, 2164795 } + }, + { + { 15995086, 3199873, 13672555, 13712240, -19378835, -4647646, -13081610, -15496269, -13492807, 1268052 }, + { -10290614, -3659039, -3286592, 10948818, 23037027, 3794475, -3470338, -12600221, -17055369, 3565904 }, + { 29210088, -9419337, -5919792, -4952785, 10834811, -13327726, -16512102, -10820713, -27162222, -14030531 } + }, + { + { -13161890, 15508588, 16663704, -8156150, -28349942, 9019123, -29183421, -3769423, 2244111, -14001979 }, + { -5152875, -3800936, -9306475, -6071583, 16243069, 14684434, -25673088, -16180800, 13491506, 4641841 }, + { 10813417, 643330, -19188515, -728916, 30292062, -16600078, 27548447, -7721242, 14476989, -12767431 } + }, + { + { 10292079, 9984945, 6481436, 8279905, -7251514, 7032743, 27282937, -1644259, -27912810, 12651324 }, + { -31185513, -813383, 22271204, 11835308, 10201545, 15351028, 17099662, 3988035, 21721536, -3148940 }, + { 10202177, -6545839, -31373232, -9574638, -32150642, -8119683, -12906320, 3852694, 13216206, 14842320 } + }, + { + { -15815640, -10601066, -6538952, -7258995, -6984659, -6581778, -31500847, 13765824, -27434397, 9900184 }, + { 14465505, -13833331, -32133984, -14738873, -27443187, 12990492, 33046193, 15796406, -7051866, -8040114 }, + { 30924417, -8279620, 6359016, -12816335, 16508377, 9071735, -25488601, 15413635, 9524356, -7018878 } + }, + { + { 12274201, -13175547, 32627641, -1785326, 6736625, 13267305, 5237659, -5109483, 15663516, 4035784 }, + { -2951309, 8903985, 17349946, 601635, -16432815, -4612556, -13732739, -15889334, -22258478, 4659091 }, + { -16916263, -4952973, -30393711, -15158821, 20774812, 15897498, 5736189, 15026997, -2178256, -13455585 } + } +}, +{ /* 15/31 */ + { + { -8858980, -2219056, 28571666, -10155518, -474467, -10105698, -3801496, 278095, 23440562, -290208 }, + { 10226241, -5928702, 15139956, 120818, -14867693, 5218603, 32937275, 11551483, -16571960, -7442864 }, + { 17932739, -12437276, -24039557, 10749060, 11316803, 7535897, 22503767, 5561594, -3646624, 3898661 } + }, + { + { 7749907, -969567, -16339731, -16464, -25018111, 15122143, -1573531, 7152530, 21831162, 1245233 }, + { 26958459, -14658026, 4314586, 8346991, -5677764, 11960072, -32589295, -620035, -30402091, -16716212 }, + { -12165896, 9166947, 33491384, 13673479, 29787085, 13096535, 6280834, 14587357, -22338025, 13987525 } + }, + { + { -24349909, 7778775, 21116000, 15572597, -4833266, -5357778, -4300898, -5124639, -7469781, -2858068 }, + { 9681908, -6737123, -31951644, 13591838, -6883821, 386950, 31622781, 6439245, -14581012, 4091397 }, + { -8426427, 1470727, -28109679, -1596990, 3978627, -5123623, -19622683, 12092163, 29077877, -14741988 } + }, + { + { 5269168, -6859726, -13230211, -8020715, 25932563, 1763552, -5606110, -5505881, -20017847, 2357889 }, + { 32264008, -15407652, -5387735, -1160093, -2091322, -3946900, 23104804, -12869908, 5727338, 189038 }, + { 14609123, -8954470, -6000566, -16622781, -14577387, -7743898, -26745169, 10942115, -25888931, -14884697 } + }, + { + { 20513500, 5557931, -15604613, 7829531, 26413943, -2019404, -21378968, 7471781, 13913677, -5137875 }, + { -25574376, 11967826, 29233242, 12948236, -6754465, 4713227, -8940970, 14059180, 12878652, 8511905 }, + { -25656801, 3393631, -2955415, -7075526, -2250709, 9366908, -30223418, 6812974, 5568676, -3127656 } + }, + { + { 11630004, 12144454, 2116339, 13606037, 27378885, 15676917, -17408753, -13504373, -14395196, 8070818 }, + { 27117696, -10007378, -31282771, -5570088, 1127282, 12772488, -29845906, 10483306, -11552749, -1028714 }, + { 10637467, -5688064, 5674781, 1072708, -26343588, -6982302, -1683975, 9177853, -27493162, 15431203 } + }, + { + { 20525145, 10892566, -12742472, 12779443, -29493034, 16150075, -28240519, 14943142, -15056790, -7935931 }, + { -30024462, 5626926, -551567, -9981087, 753598, 11981191, 25244767, -3239766, -3356550, 9594024 }, + { -23752644, 2636870, -5163910, -10103818, 585134, 7877383, 11345683, -6492290, 13352335, -10977084 } + }, + { + { -1931799, -5407458, 3304649, -12884869, 17015806, -4877091, -29783850, -7752482, -13215537, -319204 }, + { 20239939, 6607058, 6203985, 3483793, -18386976, -779229, -20723742, 15077870, -22750759, 14523817 }, + { 27406042, -6041657, 27423596, -4497394, 4996214, 10002360, -28842031, -4545494, -30172742, -4805667 } + } +}, +{ /* 16/31 */ + { + { 11374242, 12660715, 17861383, -12540833, 10935568, 1099227, -13886076, -9091740, -27727044, 11358504 }, + { -12730809, 10311867, 1510375, 10778093, -2119455, -9145702, 32676003, 11149336, -26123651, 4985768 }, + { -19096303, 341147, -6197485, -239033, 15756973, -8796662, -983043, 13794114, -19414307, -15621255 } + }, + { + { 6490081, 11940286, 25495923, -7726360, 8668373, -8751316, 3367603, 6970005, -1691065, -9004790 }, + { 1656497, 13457317, 15370807, 6364910, 13605745, 8362338, -19174622, -5475723, -16796596, -5031438 }, + { -22273315, -13524424, -64685, -4334223, -18605636, -10921968, -20571065, -7007978, -99853, -10237333 } + }, + { + { 17747465, 10039260, 19368299, -4050591, -20630635, -16041286, 31992683, -15857976, -29260363, -5511971 }, + { 31932027, -4986141, -19612382, 16366580, 22023614, 88450, 11371999, -3744247, 4882242, -10626905 }, + { 29796507, 37186, 19818052, 10115756, -11829032, 3352736, 18551198, 3272828, -5190932, -4162409 } + }, + { + { 12501286, 4044383, -8612957, -13392385, -32430052, 5136599, -19230378, -3529697, 330070, -3659409 }, + { 6384877, 2899513, 17807477, 7663917, -2358888, 12363165, 25366522, -8573892, -271295, 12071499 }, + { -8365515, -4042521, 25133448, -4517355, -6211027, 2265927, -32769618, 1936675, -5159697, 3829363 } + }, + { + { 28425966, -5835433, -577090, -4697198, -14217555, 6870930, 7921550, -6567787, 26333140, 14267664 }, + { -11067219, 11871231, 27385719, -10559544, -4585914, -11189312, 10004786, -8709488, -21761224, 8930324 }, + { -21197785, -16396035, 25654216, -1725397, 12282012, 11008919, 1541940, 4757911, -26491501, -16408940 } + }, + { + { 13537262, -7759490, -20604840, 10961927, -5922820, -13218065, -13156584, 6217254, -15943699, 13814990 }, + { -17422573, 15157790, 18705543, 29619, 24409717, -260476, 27361681, 9257833, -1956526, -1776914 }, + { -25045300, -10191966, 15366585, 15166509, -13105086, 8423556, -29171540, 12361135, -18685978, 4578290 } + }, + { + { 24579768, 3711570, 1342322, -11180126, -27005135, 14124956, -22544529, 14074919, 21964432, 8235257 }, + { -6528613, -2411497, 9442966, -5925588, 12025640, -1487420, -2981514, -1669206, 13006806, 2355433 }, + { -16304899, -13605259, -6632427, -5142349, 16974359, -10911083, 27202044, 1719366, 1141648, -12796236 } + }, + { + { -12863944, -13219986, -8318266, -11018091, -6810145, -4843894, 13475066, -3133972, 32674895, 13715045 }, + { 11423335, -5468059, 32344216, 8962751, 24989809, 9241752, -13265253, 16086212, -28740881, -15642093 }, + { -1409668, 12530728, -6368726, 10847387, 19531186, -14132160, -11709148, 7791794, -27245943, 4383347 } + } +}, +{ /* 17/31 */ + { + { -28970898, 5271447, -1266009, -9736989, -12455236, 16732599, -4862407, -4906449, 27193557, 6245191 }, + { -15193956, 5362278, -1783893, 2695834, 4960227, 12840725, 23061898, 3260492, 22510453, 8577507 }, + { -12632451, 11257346, -32692994, 13548177, -721004, 10879011, 31168030, 13952092, -29571492, -3635906 } + }, + { + { 3877321, -9572739, 32416692, 5405324, -11004407, -13656635, 3759769, 11935320, 5611860, 8164018 }, + { -16275802, 14667797, 15906460, 12155291, -22111149, -9039718, 32003002, -8832289, 5773085, -8422109 }, + { -23788118, -8254300, 1950875, 8937633, 18686727, 16459170, -905725, 12376320, 31632953, 190926 } + }, + { + { -24593607, -16138885, -8423991, 13378746, 14162407, 6901328, -8288749, 4508564, -25341555, -3627528 }, + { 8884438, -5884009, 6023974, 10104341, -6881569, -4941533, 18722941, -14786005, -1672488, 827625 }, + { -32720583, -16289296, -32503547, 7101210, 13354605, 2659080, -1800575, -14108036, -24878478, 1541286 } + }, + { + { 2901347, -1117687, 3880376, -10059388, -17620940, -3612781, -21802117, -3567481, 20456845, -1885033 }, + { 27019610, 12299467, -13658288, -1603234, -12861660, -4861471, -19540150, -5016058, 29439641, 15138866 }, + { 21536104, -6626420, -32447818, -10690208, -22408077, 5175814, -5420040, -16361163, 7779328, 109896 } + }, + { + { 30279744, 14648750, -8044871, 6425558, 13639621, -743509, 28698390, 12180118, 23177719, -554075 }, + { 26572847, 3405927, -31701700, 12890905, -19265668, 5335866, -6493768, 2378492, 4439158, -13279347 }, + { -22716706, 3489070, -9225266, -332753, 18875722, -1140095, 14819434, -12731527, -17717757, -5461437 } + }, + { + { -5056483, 16566551, 15953661, 3767752, -10436499, 15627060, -820954, 2177225, 8550082, -15114165 }, + { -18473302, 16596775, -381660, 15663611, 22860960, 15585581, -27844109, -3582739, -23260460, -8428588 }, + { -32480551, 15707275, -8205912, -5652081, 29464558, 2713815, -22725137, 15860482, -21902570, 1494193 } + }, + { + { -19562091, -14087393, -25583872, -9299552, 13127842, 759709, 21923482, 16529112, 8742704, 12967017 }, + { -28464899, 1553205, 32536856, -10473729, -24691605, -406174, -8914625, -2933896, -29903758, 15553883 }, + { 21877909, 3230008, 9881174, 10539357, -4797115, 2841332, 11543572, 14513274, 19375923, -12647961 } + }, + { + { 8832269, -14495485, 13253511, 5137575, 5037871, 4078777, 24880818, -6222716, 2862653, 9455043 }, + { 29306751, 5123106, 20245049, -14149889, 9592566, 8447059, -2077124, -2990080, 15511449, 4789663 }, + { -20679756, 7004547, 8824831, -9434977, -4045704, -3750736, -5754762, 108893, 23513200, 16652362 } + } +}, +{ /* 18/31 */ + { + { -33256173, 4144782, -4476029, -6579123, 10770039, -7155542, -6650416, -12936300, -18319198, 10212860 }, + { 2756081, 8598110, 7383731, -6859892, 22312759, -1105012, 21179801, 2600940, -9988298, -12506466 }, + { -24645692, 13317462, -30449259, -15653928, 21365574, -10869657, 11344424, 864440, -2499677, -16710063 } + }, + { + { -26432803, 6148329, -17184412, -14474154, 18782929, -275997, -22561534, 211300, 2719757, 4940997 }, + { -1323882, 3911313, -6948744, 14759765, -30027150, 7851207, 21690126, 8518463, 26699843, 5276295 }, + { -13149873, -6429067, 9396249, 365013, 24703301, -10488939, 1321586, 149635, -15452774, 7159369 } + }, + { + { 9987780, -3404759, 17507962, 9505530, 9731535, -2165514, 22356009, 8312176, 22477218, -8403385 }, + { 18155857, -16504990, 19744716, 9006923, 15154154, -10538976, 24256460, -4864995, -22548173, 9334109 }, + { 2986088, -4911893, 10776628, -3473844, 10620590, -7083203, -21413845, 14253545, -22587149, 536906 } + }, + { + { 4377756, 8115836, 24567078, 15495314, 11625074, 13064599, 7390551, 10589625, 10838060, -15420424 }, + { -19342404, 867880, 9277171, -3218459, -14431572, -1986443, 19295826, -15796950, 6378260, 699185 }, + { 7895026, 4057113, -7081772, -13077756, -17886831, -323126, -716039, 15693155, -5045064, -13373962 } + }, + { + { -7737563, -5869402, -14566319, -7406919, 11385654, 13201616, 31730678, -10962840, -3918636, -9669325 }, + { 10188286, -15770834, -7336361, 13427543, 22223443, 14896287, 30743455, 7116568, -21786507, 5427593 }, + { 696102, 13206899, 27047647, -10632082, 15285305, -9853179, 10798490, -4578720, 19236243, 12477404 } + }, + { + { -11229439, 11243796, -17054270, -8040865, -788228, -8167967, -3897669, 11180504, -23169516, 7733644 }, + { 17800790, -14036179, -27000429, -11766671, 23887827, 3149671, 23466177, -10538171, 10322027, 15313801 }, + { 26246234, 11968874, 32263343, -5468728, 6830755, -13323031, -15794704, -101982, -24449242, 10890804 } + }, + { + { -31365647, 10271363, -12660625, -6267268, 16690207, -13062544, -14982212, 16484931, 25180797, -5334884 }, + { -586574, 10376444, -32586414, -11286356, 19801893, 10997610, 2276632, 9482883, 316878, 13820577 }, + { -9882808, -4510367, -2115506, 16457136, -11100081, 11674996, 30756178, -7515054, 30696930, -3712849 } + }, + { + { 32988917, -9603412, 12499366, 7910787, -10617257, -11931514, -7342816, -9985397, -32349517, 7392473 }, + { -8855661, 15927861, 9866406, -3649411, -2396914, -16655781, -30409476, -9134995, 25112947, -2926644 }, + { -2504044, -436966, 25621774, -5678772, 15085042, -5479877, -24884878, -13526194, 5537438, -13914319 } + } +}, +{ /* 19/31 */ + { + { -11225584, 2320285, -9584280, 10149187, -33444663, 5808648, -14876251, -1729667, 31234590, 6090599 }, + { -9633316, 116426, 26083934, 2897444, -6364437, -2688086, 609721, 15878753, -6970405, -9034768 }, + { -27757857, 247744, -15194774, -9002551, 23288161, -10011936, -23869595, 6503646, 20650474, 1804084 } + }, + { + { -27589786, 15456424, 8972517, 8469608, 15640622, 4439847, 3121995, -10329713, 27842616, -202328 }, + { -15306973, 2839644, 22530074, 10026331, 4602058, 5048462, 28248656, 5031932, -11375082, 12714369 }, + { 20807691, -7270825, 29286141, 11421711, -27876523, -13868230, -21227475, 1035546, -19733229, 12796920 } + }, + { + { 12076899, -14301286, -8785001, -11848922, -25012791, 16400684, -17591495, -12899438, 3480665, -15182815 }, + { -32361549, 5457597, 28548107, 7833186, 7303070, -11953545, -24363064, -15921875, -33374054, 2771025 }, + { -21389266, 421932, 26597266, 6860826, 22486084, -6737172, -17137485, -4210226, -24552282, 15673397 } + }, + { + { -20184622, 2338216, 19788685, -9620956, -4001265, -8740893, -20271184, 4733254, 3727144, -12934448 }, + { 6120119, 814863, -11794402, -622716, 6812205, -15747771, 2019594, 7975683, 31123697, -10958981 }, + { 30069250, -11435332, 30434654, 2958439, 18399564, -976289, 12296869, 9204260, -16432438, 9648165 } + }, + { + { 32705432, -1550977, 30705658, 7451065, -11805606, 9631813, 3305266, 5248604, -26008332, -11377501 }, + { 17219865, 2375039, -31570947, -5575615, -19459679, 9219903, 294711, 15298639, 2662509, -16297073 }, + { -1172927, -7558695, -4366770, -4287744, -21346413, -8434326, 32087529, -1222777, 32247248, -14389861 } + }, + { + { 14312628, 1221556, 17395390, -8700143, -4945741, -8684635, -28197744, -9637817, -16027623, -13378845 }, + { -1428825, -9678990, -9235681, 6549687, -7383069, -468664, 23046502, 9803137, 17597934, 2346211 }, + { 18510800, 15337574, 26171504, 981392, -22241552, 7827556, -23491134, -11323352, 3059833, -11782870 } + }, + { + { 10141598, 6082907, 17829293, -1947643, 9830092, 13613136, -25556636, -5544586, -33502212, 3592096 }, + { 33114168, -15889352, -26525686, -13343397, 33076705, 8716171, 1151462, 1521897, -982665, -6837803 }, + { -32939165, -4255815, 23947181, -324178, -33072974, -12305637, -16637686, 3891704, 26353178, 693168 } + }, + { + { 30374239, 1595580, -16884039, 13186931, 4600344, 406904, 9585294, -400668, 31375464, 14369965 }, + { -14370654, -7772529, 1510301, 6434173, -18784789, -6262728, 32732230, -13108839, 17901441, 16011505 }, + { 18171223, -11934626, -12500402, 15197122, -11038147, -15230035, -19172240, -16046376, 8764035, 12309598 } + } +}, +{ /* 20/31 */ + { + { 5975908, -5243188, -19459362, -9681747, -11541277, 14015782, -23665757, 1228319, 17544096, -10593782 }, + { 5811932, -1715293, 3442887, -2269310, -18367348, -8359541, -18044043, -15410127, -5565381, 12348900 }, + { -31399660, 11407555, 25755363, 6891399, -3256938, 14872274, -24849353, 8141295, -10632534, -585479 } + }, + { + { -12675304, 694026, -5076145, 13300344, 14015258, -14451394, -9698672, -11329050, 30944593, 1130208 }, + { 8247766, -6710942, -26562381, -7709309, -14401939, -14648910, 4652152, 2488540, 23550156, -271232 }, + { 17294316, -3788438, 7026748, 15626851, 22990044, 113481, 2267737, -5908146, -408818, -137719 } + }, + { + { 16091085, -16253926, 18599252, 7340678, 2137637, -1221657, -3364161, 14550936, 3260525, -7166271 }, + { -4910104, -13332887, 18550887, 10864893, -16459325, -7291596, -23028869, -13204905, -12748722, 2701326 }, + { -8574695, 16099415, 4629974, -16340524, -20786213, -6005432, -10018363, 9276971, 11329923, 1862132 } + }, + { + { 14763076, -15903608, -30918270, 3689867, 3511892, 10313526, -21951088, 12219231, -9037963, -940300 }, + { 8894987, -3446094, 6150753, 3013931, 301220, 15693451, -31981216, -2909717, -15438168, 11595570 }, + { 15214962, 3537601, -26238722, -14058872, 4418657, -15230761, 13947276, 10730794, -13489462, -4363670 } + }, + { + { -2538306, 7682793, 32759013, 263109, -29984731, -7955452, -22332124, -10188635, 977108, 699994 }, + { -12466472, 4195084, -9211532, 550904, -15565337, 12917920, 19118110, -439841, -30534533, -14337913 }, + { 31788461, -14507657, 4799989, 7372237, 8808585, -14747943, 9408237, -10051775, 12493932, -5409317 } + }, + { + { -25680606, 5260744, -19235809, -6284470, -3695942, 16566087, 27218280, 2607121, 29375955, 6024730 }, + { 842132, -2794693, -4763381, -8722815, 26332018, -12405641, 11831880, 6985184, -9940361, 2854096 }, + { -4847262, -7969331, 2516242, -5847713, 9695691, -7221186, 16512645, 960770, 12121869, 16648078 } + }, + { + { -15218652, 14667096, -13336229, 2013717, 30598287, -464137, -31504922, -7882064, 20237806, 2838411 }, + { -19288047, 4453152, 15298546, -16178388, 22115043, -15972604, 12544294, -13470457, 1068881, -12499905 }, + { -9558883, -16518835, 33238498, 13506958, 30505848, -1114596, -8486907, -2630053, 12521378, 4845654 } + }, + { + { -28198521, 10744108, -2958380, 10199664, 7759311, -13088600, 3409348, -873400, -6482306, -12885870 }, + { -23561822, 6230156, -20382013, 10655314, -24040585, -11621172, 10477734, -1240216, -3113227, 13974498 }, + { 12966261, 15550616, -32038948, -1615346, 21025980, -629444, 5642325, 7188737, 18895762, 12629579 } + } +}, +{ /* 21/31 */ + { + { 14741879, -14946887, 22177208, -11721237, 1279741, 8058600, 11758140, 789443, 32195181, 3895677 }, + { 10758205, 15755439, -4509950, 9243698, -4879422, 6879879, -2204575, -3566119, -8982069, 4429647 }, + { -2453894, 15725973, -20436342, -10410672, -5803908, -11040220, -7135870, -11642895, 18047436, -15281743 } + }, + { + { -25173001, -11307165, 29759956, 11776784, -22262383, -15820455, 10993114, -12850837, -17620701, -9408468 }, + { 21987233, 700364, -24505048, 14972008, -7774265, -5718395, 32155026, 2581431, -29958985, 8773375 }, + { -25568350, 454463, -13211935, 16126715, 25240068, 8594567, 20656846, 12017935, -7874389, -13920155 } + }, + { + { 6028182, 6263078, -31011806, -11301710, -818919, 2461772, -31841174, -5468042, -1721788, -2776725 }, + { -12278994, 16624277, 987579, -5922598, 32908203, 1248608, 7719845, -4166698, 28408820, 6816612 }, + { -10358094, -8237829, 19549651, -12169222, 22082623, 16147817, 20613181, 13982702, -10339570, 5067943 } + }, + { + { -30505967, -3821767, 12074681, 13582412, -19877972, 2443951, -19719286, 12746132, 5331210, -10105944 }, + { 30528811, 3601899, -1957090, 4619785, -27361822, -15436388, 24180793, -12570394, 27679908, -1648928 }, + { 9402404, -13957065, 32834043, 10838634, -26580150, -13237195, 26653274, -8685565, 22611444, -12715406 } + }, + { + { 22190590, 1118029, 22736441, 15130463, -30460692, -5991321, 19189625, -4648942, 4854859, 6622139 }, + { -8310738, -2953450, -8262579, -3388049, -10401731, -271929, 13424426, -3567227, 26404409, 13001963 }, + { -31241838, -15415700, -2994250, 8939346, 11562230, -12840670, -26064365, -11621720, -15405155, 11020693 } + }, + { + { 1866042, -7949489, -7898649, -10301010, 12483315, 13477547, 3175636, -12424163, 28761762, 1406734 }, + { -448555, -1777666, 13018551, 3194501, -9580420, -11161737, 24760585, -4347088, 25577411, -13378680 }, + { -24290378, 4759345, -690653, -1852816, 2066747, 10693769, -29595790, 9884936, -9368926, 4745410 } + }, + { + { -9141284, 6049714, -19531061, -4341411, -31260798, 9944276, -15462008, -11311852, 10931924, -11931931 }, + { -16561513, 14112680, -8012645, 4817318, -8040464, -11414606, -22853429, 10856641, -20470770, 13434654 }, + { 22759489, -10073434, -16766264, -1871422, 13637442, -10168091, 1765144, -12654326, 28445307, -5364710 } + }, + { + { 29875063, 12493613, 2795536, -3786330, 1710620, 15181182, -10195717, -8788675, 9074234, 1167180 }, + { -26205683, 11014233, -9842651, -2635485, -26908120, 7532294, -18716888, -9535498, 3843903, 9367684 }, + { -10969595, -6403711, 9591134, 9582310, 11349256, 108879, 16235123, 8601684, -139197, 4242895 } + } +}, +{ /* 22/31 */ + { + { 22092954, -13191123, -2042793, -11968512, 32186753, -11517388, -6574341, 2470660, -27417366, 16625501 }, + { -11057722, 3042016, 13770083, -9257922, 584236, -544855, -7770857, 2602725, -27351616, 14247413 }, + { 6314175, -10264892, -32772502, 15957557, -10157730, 168750, -8618807, 14290061, 27108877, -1180880 } + }, + { + { -8586597, -7170966, 13241782, 10960156, -32991015, -13794596, 33547976, -11058889, -27148451, 981874 }, + { 22833440, 9293594, -32649448, -13618667, -9136966, 14756819, -22928859, -13970780, -10479804, -16197962 }, + { -7768587, 3326786, -28111797, 10783824, 19178761, 14905060, 22680049, 13906969, -15933690, 3797899 } + }, + { + { 21721356, -4212746, -12206123, 9310182, -3882239, -13653110, 23740224, -2709232, 20491983, -8042152 }, + { 9209270, -15135055, -13256557, -6167798, -731016, 15289673, 25947805, 15286587, 30997318, -6703063 }, + { 7392032, 16618386, 23946583, -8039892, -13265164, -1533858, -14197445, -2321576, 17649998, -250080 } + }, + { + { -9301088, -14193827, 30609526, -3049543, -25175069, -1283752, -15241566, -9525724, -2233253, 7662146 }, + { -17558673, 1763594, -33114336, 15908610, -30040870, -12174295, 7335080, -8472199, -3174674, 3440183 }, + { -19889700, -5977008, -24111293, -9688870, 10799743, -16571957, 40450, -4431835, 4862400, 1133 } + }, + { + { -32856209, -7873957, -5422389, 14860950, -16319031, 7956142, 7258061, 311861, -30594991, -7379421 }, + { -3773428, -1565936, 28985340, 7499440, 24445838, 9325937, 29727763, 16527196, 18278453, 15405622 }, + { -4381906, 8508652, -19898366, -3674424, -5984453, 15149970, -13313598, 843523, -21875062, 13626197 } + }, + { + { 2281448, -13487055, -10915418, -2609910, 1879358, 16164207, -10783882, 3953792, 13340839, 15928663 }, + { 31727126, -7179855, -18437503, -8283652, 2875793, -16390330, -25269894, -7014826, -23452306, 5964753 }, + { 4100420, -5959452, -17179337, 6017714, -18705837, 12227141, -26684835, 11344144, 2538215, -7570755 } + }, + { + { -9433605, 6123113, 11159803, -2156608, 30016280, 14966241, -20474983, 1485421, -629256, -15958862 }, + { -26804558, 4260919, 11851389, 9658551, -32017107, 16367492, -20205425, -13191288, 11659922, -11115118 }, + { 26180396, 10015009, -30844224, -8581293, 5418197, 9480663, 2231568, -10170080, 33100372, -1306171 } + }, + { + { 15121113, -5201871, -10389905, 15427821, -27509937, -15992507, 21670947, 4486675, -5931810, -14466380 }, + { 16166486, -9483733, -11104130, 6023908, -31926798, -1364923, 2340060, -16254968, -10735770, -10039824 }, + { 28042865, -3557089, -12126526, 12259706, -3717498, -6945899, 6766453, -8689599, 18036436, 5803270 } + } +}, +{ /* 23/31 */ + { + { -817581, 6763912, 11803561, 1585585, 10958447, -2671165, 23855391, 4598332, -6159431, -14117438 }, + { -31031306, -14256194, 17332029, -2383520, 31312682, -5967183, 696309, 50292, -20095739, 11763584 }, + { -594563, -2514283, -32234153, 12643980, 12650761, 14811489, 665117, -12613632, -19773211, -10713562 } + }, + { + { 30464590, -11262872, -4127476, -12734478, 19835327, -7105613, -24396175, 2075773, -17020157, 992471 }, + { 18357185, -6994433, 7766382, 16342475, -29324918, 411174, 14578841, 8080033, -11574335, -10601610 }, + { 19598397, 10334610, 12555054, 2555664, 18821899, -10339780, 21873263, 16014234, 26224780, 16452269 } + }, + { + { -30223925, 5145196, 5944548, 16385966, 3976735, 2009897, -11377804, -7618186, -20533829, 3698650 }, + { 14187449, 3448569, -10636236, -10810935, -22663880, -3433596, 7268410, -10890444, 27394301, 12015369 }, + { 19695761, 16087646, 28032085, 12999827, 6817792, 11427614, 20244189, -1312777, -13259127, -3402461 } + }, + { + { 30860103, 12735208, -1888245, -4699734, -16974906, 2256940, -8166013, 12298312, -8550524, -10393462 }, + { -5719826, -11245325, -1910649, 15569035, 26642876, -7587760, -5789354, -15118654, -4976164, 12651793 }, + { -2848395, 9953421, 11531313, -5282879, 26895123, -12697089, -13118820, -16517902, 9768698, -2533218 } + }, + { + { -24719459, 1894651, -287698, -4704085, 15348719, -8156530, 32767513, 12765450, 4940095, 10678226 }, + { 18860224, 15980149, -18987240, -1562570, -26233012, -11071856, -7843882, 13944024, -24372348, 16582019 }, + { -15504260, 4970268, -29893044, 4175593, -20993212, -2199756, -11704054, 15444560, -11003761, 7989037 } + }, + { + { 31490452, 5568061, -2412803, 2182383, -32336847, 4531686, -32078269, 6200206, -19686113, -14800171 }, + { -17308668, -15879940, -31522777, -2831, -32887382, 16375549, 8680158, -16371713, 28550068, -6857132 }, + { -28126887, -5688091, 16837845, -1820458, -6850681, 12700016, -30039981, 4364038, 1155602, 5988841 } + }, + { + { 21890435, -13272907, -12624011, 12154349, -7831873, 15300496, 23148983, -4470481, 24618407, 8283181 }, + { -33136107, -10512751, 9975416, 6841041, -31559793, 16356536, 3070187, -7025928, 1466169, 10740210 }, + { -1509399, -15488185, -13503385, -10655916, 32799044, 909394, -13938903, -5779719, -32164649, -15327040 } + }, + { + { 3960823, -14267803, -28026090, -15918051, -19404858, 13146868, 15567327, 951507, -3260321, -573935 }, + { 24740841, 5052253, -30094131, 8961361, 25877428, 6165135, -24368180, 14397372, -7380369, -6144105 }, + { -28888365, 3510803, -28103278, -1158478, -11238128, -10631454, -15441463, -14453128, -1625486, -6494814 } + } +}, +{ /* 24/31 */ + { + { 793299, -9230478, 8836302, -6235707, -27360908, -2369593, 33152843, -4885251, -9906200, -621852 }, + { 5666233, 525582, 20782575, -8038419, -24538499, 14657740, 16099374, 1468826, -6171428, -15186581 }, + { -4859255, -3779343, -2917758, -6748019, 7778750, 11688288, -30404353, -9871238, -1558923, -9863646 } + }, + { + { 10896332, -7719704, 824275, 472601, -19460308, 3009587, 25248958, 14783338, -30581476, -15757844 }, + { 10566929, 12612572, -31944212, 11118703, -12633376, 12362879, 21752402, 8822496, 24003793, 14264025 }, + { 27713862, -7355973, -11008240, 9227530, 27050101, 2504721, 23886875, -13117525, 13958495, -5732453 } + }, + { + { -23481610, 4867226, -27247128, 3900521, 29838369, -8212291, -31889399, -10041781, 7340521, -15410068 }, + { 4646514, -8011124, -22766023, -11532654, 23184553, 8566613, 31366726, -1381061, -15066784, -10375192 }, + { -17270517, 12723032, -16993061, 14878794, 21619651, -6197576, 27584817, 3093888, -8843694, 3849921 } + }, + { + { -9064912, 2103172, 25561640, -15125738, -5239824, 9582958, 32477045, -9017955, 5002294, -15550259 }, + { -12057553, -11177906, 21115585, -13365155, 8808712, -12030708, 16489530, 13378448, -25845716, 12741426 }, + { -5946367, 10645103, -30911586, 15390284, -3286982, -7118677, 24306472, 15852464, 28834118, -7646072 } + }, + { + { -17335748, -9107057, -24531279, 9434953, -8472084, -583362, -13090771, 455841, 20461858, 5491305 }, + { 13669248, -16095482, -12481974, -10203039, -14569770, -11893198, -24995986, 11293807, -28588204, -9421832 }, + { 28497928, 6272777, -33022994, 14470570, 8906179, -1225630, 18504674, -14165166, 29867745, -8795943 } + }, + { + { -16207023, 13517196, -27799630, -13697798, 24009064, -6373891, -6367600, -13175392, 22853429, -4012011 }, + { 24191378, 16712145, -13931797, 15217831, 14542237, 1646131, 18603514, -11037887, 12876623, -2112447 }, + { 17902668, 4518229, -411702, -2829247, 26878217, 5258055, -12860753, 608397, 16031844, 3723494 } + }, + { + { -28632773, 12763728, -20446446, 7577504, 33001348, -13017745, 17558842, -7872890, 23896954, -4314245 }, + { -20005381, -12011952, 31520464, 605201, 2543521, 5991821, -2945064, 7229064, -9919646, -8826859 }, + { 28816045, 298879, -28165016, -15920938, 19000928, -1665890, -12680833, -2949325, -18051778, -2082915 } + }, + { + { 16000882, -344896, 3493092, -11447198, -29504595, -13159789, 12577740, 16041268, -19715240, 7847707 }, + { 10151868, 10572098, 27312476, 7922682, 14825339, 4723128, -32855931, -6519018, -10020567, 3852848 }, + { -11430470, 15697596, -21121557, -4420647, 5386314, 15063598, 16514493, -15932110, 29330899, -15076224 } + } +}, +{ /* 25/31 */ + { + { -25499735, -4378794, -15222908, -6901211, 16615731, 2051784, 3303702, 15490, -27548796, 12314391 }, + { 15683520, -6003043, 18109120, -9980648, 15337968, -5997823, -16717435, 15921866, 16103996, -3731215 }, + { -23169824, -10781249, 13588192, -1628807, -3798557, -1074929, -19273607, 5402699, -29815713, -9841101 } + }, + { + { 23190676, 2384583, -32714340, 3462154, -29903655, -1529132, -11266856, 8911517, -25205859, 2739713 }, + { 21374101, -3554250, -33524649, 9874411, 15377179, 11831242, -33529904, 6134907, 4931255, 11987849 }, + { -7732, -2978858, -16223486, 7277597, 105524, -322051, -31480539, 13861388, -30076310, 10117930 } + }, + { + { -29501170, -10744872, -26163768, 13051539, -25625564, 5089643, -6325503, 6704079, 12890019, 15728940 }, + { -21972360, -11771379, -951059, -4418840, 14704840, 2695116, 903376, -10428139, 12885167, 8311031 }, + { -17516482, 5352194, 10384213, -13811658, 7506451, 13453191, 26423267, 4384730, 1888765, -5435404 } + }, + { + { -25817338, -3107312, -13494599, -3182506, 30896459, -13921729, -32251644, -12707869, -19464434, -3340243 }, + { -23607977, -2665774, -526091, 4651136, 5765089, 4618330, 6092245, 14845197, 17151279, -9854116 }, + { -24830458, -12733720, -15165978, 10367250, -29530908, -265356, 22825805, -7087279, -16866484, 16176525 } + }, + { + { -23583256, 6564961, 20063689, 3798228, -4740178, 7359225, 2006182, -10363426, -28746253, -10197509 }, + { -10626600, -4486402, -13320562, -5125317, 3432136, -6393229, 23632037, -1940610, 32808310, 1099883 }, + { 15030977, 5768825, -27451236, -2887299, -6427378, -15361371, -15277896, -6809350, 2051441, -15225865 } + }, + { + { -3362323, -7239372, 7517890, 9824992, 23555850, 295369, 5148398, -14154188, -22686354, 16633660 }, + { 4577086, -16752288, 13249841, -15304328, 19958763, -14537274, 18559670, -10759549, 8402478, -9864273 }, + { -28406330, -1051581, -26790155, -907698, -17212414, -11030789, 9453451, -14980072, 17983010, 9967138 } + }, + { + { -25762494, 6524722, 26585488, 9969270, 24709298, 1220360, -1677990, 7806337, 17507396, 3651560 }, + { -10420457, -4118111, 14584639, 15971087, -15768321, 8861010, 26556809, -5574557, -18553322, -11357135 }, + { 2839101, 14284142, 4029895, 3472686, 14402957, 12689363, -26642121, 8459447, -5605463, -7621941 } + }, + { + { -4839289, -3535444, 9744961, 2871048, 25113978, 3187018, -25110813, -849066, 17258084, -7977739 }, + { 18164541, -10595176, -17154882, -1542417, 19237078, -9745295, 23357533, -15217008, 26908270, 12150756 }, + { -30264870, -7647865, 5112249, -7036672, -1499807, -6974257, 43168, -5537701, -32302074, 16215819 } + } +}, +{ /* 26/31 */ + { + { -6898905, 9824394, -12304779, -4401089, -31397141, -6276835, 32574489, 12532905, -7503072, -8675347 }, + { -27343522, -16515468, -27151524, -10722951, 946346, 16291093, 254968, 7168080, 21676107, -1943028 }, + { 21260961, -8424752, -16831886, -11920822, -23677961, 3968121, -3651949, -6215466, -3556191, -7913075 } + }, + { + { 16544754, 13250366, -16804428, 15546242, -4583003, 12757258, -2462308, -8680336, -18907032, -9662799 }, + { -2415239, -15577728, 18312303, 4964443, -15272530, -12653564, 26820651, 16690659, 25459437, -4564609 }, + { -25144690, 11425020, 28423002, -11020557, -6144921, -15826224, 9142795, -2391602, -6432418, -1644817 } + }, + { + { -23104652, 6253476, 16964147, -3768872, -25113972, -12296437, -27457225, -16344658, 6335692, 7249989 }, + { -30333227, 13979675, 7503222, -12368314, -11956721, -4621693, -30272269, 2682242, 25993170, -12478523 }, + { 4364628, 5930691, 32304656, -10044554, -8054781, 15091131, 22857016, -10598955, 31820368, 15075278 } + }, + { + { 31879134, -8918693, 17258761, 90626, -8041836, -4917709, 24162788, -9650886, -17970238, 12833045 }, + { 19073683, 14851414, -24403169, -11860168, 7625278, 11091125, -19619190, 2074449, -9413939, 14905377 }, + { 24483667, -11935567, -2518866, -11547418, -1553130, 15355506, -25282080, 9253129, 27628530, -7555480 } + }, + { + { 17597607, 8340603, 19355617, 552187, 26198470, -3176583, 4593324, -9157582, -14110875, 15297016 }, + { 510886, 14337390, -31785257, 16638632, 6328095, 2713355, -20217417, -11864220, 8683221, 2921426 }, + { 18606791, 11874196, 27155355, -5281482, -24031742, 6265446, -25178240, -1278924, 4674690, 13890525 } + }, + { + { 13609624, 13069022, -27372361, -13055908, 24360586, 9592974, 14977157, 9835105, 4389687, 288396 }, + { 9922506, -519394, 13613107, 5883594, -18758345, -434263, -12304062, 8317628, 23388070, 16052080 }, + { 12720016, 11937594, -31970060, -5028689, 26900120, 8561328, -20155687, -11632979, -14754271, -10812892 } + }, + { + { 15961858, 14150409, 26716931, -665832, -22794328, 13603569, 11829573, 7467844, -28822128, 929275 }, + { 11038231, -11582396, -27310482, -7316562, -10498527, -16307831, -23479533, -9371869, -21393143, 2465074 }, + { 20017163, -4323226, 27915242, 1529148, 12396362, 15675764, 13817261, -9658066, 2463391, -4622140 } + }, + { + { -16358878, -12663911, -12065183, 4996454, -1256422, 1073572, 9583558, 12851107, 4003896, 12673717 }, + { -1731589, -15155870, -3262930, 16143082, 19294135, 13385325, 14741514, -9103726, 7903886, 2348101 }, + { 24536016, -16515207, 12715592, -3862155, 1511293, 10047386, -3842346, -7129159, -28377538, 10048127 } + } +}, +{ /* 27/31 */ + { + { -12622226, -6204820, 30718825, 2591312, -10617028, 12192840, 18873298, -7297090, -32297756, 15221632 }, + { -26478122, -11103864, 11546244, -1852483, 9180880, 7656409, -21343950, 2095755, 29769758, 6593415 }, + { -31994208, -2907461, 4176912, 3264766, 12538965, -868111, 26312345, -6118678, 30958054, 8292160 } + }, + { + { 31429822, -13959116, 29173532, 15632448, 12174511, -2760094, 32808831, 3977186, 26143136, -3148876 }, + { 22648901, 1402143, -22799984, 13746059, 7936347, 365344, -8668633, -1674433, -3758243, -2304625 }, + { -15491917, 8012313, -2514730, -12702462, -23965846, -10254029, -1612713, -1535569, -16664475, 8194478 } + }, + { + { 27338066, -7507420, -7414224, 10140405, -19026427, -6589889, 27277191, 8855376, 28572286, 3005164 }, + { 26287124, 4821776, 25476601, -4145903, -3764513, -15788984, -18008582, 1182479, -26094821, -13079595 }, + { -7171154, 3178080, 23970071, 6201893, -17195577, -4489192, -21876275, -13982627, 32208683, -1198248 } + }, + { + { -16657702, 2817643, -10286362, 14811298, 6024667, 13349505, -27315504, -10497842, -27672585, -11539858 }, + { 15941029, -9405932, -21367050, 8062055, 31876073, -238629, -15278393, -1444429, 15397331, -4130193 }, + { 8934485, -13485467, -23286397, -13423241, -32446090, 14047986, 31170398, -1441021, -27505566, 15087184 } + }, + { + { -18357243, -2156491, 24524913, -16677868, 15520427, -6360776, -15502406, 11461896, 16788528, -5868942 }, + { -1947386, 16013773, 21750665, 3714552, -17401782, -16055433, -3770287, -10323320, 31322514, -11615635 }, + { 21426655, -5650218, -13648287, -5347537, -28812189, -4920970, -18275391, -14621414, 13040862, -12112948 } + }, + { + { 11293895, 12478086, -27136401, 15083750, -29307421, 14748872, 14555558, -13417103, 1613711, 4896935 }, + { -25894883, 15323294, -8489791, -8057900, 25967126, -13425460, 2825960, -4897045, -23971776, -11267415 }, + { -15924766, -5229880, -17443532, 6410664, 3622847, 10243618, 20615400, 12405433, -23753030, -8436416 } + }, + { + { -7091295, 12556208, -20191352, 9025187, -17072479, 4333801, 4378436, 2432030, 23097949, -566018 }, + { 4565804, -16025654, 20084412, -7842817, 1724999, 189254, 24767264, 10103221, -18512313, 2424778 }, + { 366633, -11976806, 8173090, -6890119, 30788634, 5745705, -7168678, 1344109, -3642553, 12412659 } + }, + { + { -24001791, 7690286, 14929416, -168257, -32210835, -13412986, 24162697, -15326504, -3141501, 11179385 }, + { 18289522, -14724954, 8056945, 16430056, -21729724, 7842514, -6001441, -1486897, -18684645, -11443503 }, + { 476239, 6601091, -6152790, -9723375, 17503545, -4863900, 27672959, 13403813, 11052904, 5219329 } + } +}, +{ /* 28/31 */ + { + { 20678546, -8375738, -32671898, 8849123, -5009758, 14574752, 31186971, -3973730, 9014762, -8579056 }, + { -13644050, -10350239, -15962508, 5075808, -1514661, -11534600, -33102500, 9160280, 8473550, -3256838 }, + { 24900749, 14435722, 17209120, -15292541, -22592275, 9878983, -7689309, -16335821, -24568481, 11788948 } + }, + { + { -3118155, -11395194, -13802089, 14797441, 9652448, -6845904, -20037437, 10410733, -24568470, -1458691 }, + { -15659161, 16736706, -22467150, 10215878, -9097177, 7563911, 11871841, -12505194, -18513325, 8464118 }, + { -23400612, 8348507, -14585951, -861714, -3950205, -6373419, 14325289, 8628612, 33313881, -8370517 } + }, + { + { -20186973, -4967935, 22367356, 5271547, -1097117, -4788838, -24805667, -10236854, -8940735, -5818269 }, + { -6948785, -1795212, -32625683, -16021179, 32635414, -7374245, 15989197, -12838188, 28358192, -4253904 }, + { -23561781, -2799059, -32351682, -1661963, -9147719, 10429267, -16637684, 4072016, -5351664, 5596589 } + }, + { + { -28236598, -3390048, 12312896, 6213178, 3117142, 16078565, 29266239, 2557221, 1768301, 15373193 }, + { -7243358, -3246960, -4593467, -7553353, -127927, -912245, -1090902, -4504991, -24660491, 3442910 }, + { -30210571, 5124043, 14181784, 8197961, 18964734, -11939093, 22597931, 7176455, -18585478, 13365930 } + }, + { + { -7877390, -1499958, 8324673, 4690079, 6261860, 890446, 24538107, -8570186, -9689599, -3031667 }, + { 25008904, -10771599, -4305031, -9638010, 16265036, 15721635, 683793, -11823784, 15723479, -15163481 }, + { -9660625, 12374379, -27006999, -7026148, -7724114, -12314514, 11879682, 5400171, 519526, -1235876 } + }, + { + { 22258397, -16332233, -7869817, 14613016, -22520255, -2950923, -20353881, 7315967, 16648397, 7605640 }, + { -8081308, -8464597, -8223311, 9719710, 19259459, -15348212, 23994942, -5281555, -9468848, 4763278 }, + { -21699244, 9220969, -15730624, 1084137, -25476107, -2852390, 31088447, -7764523, -11356529, 728112 } + }, + { + { 26047220, -11751471, -6900323, -16521798, 24092068, 9158119, -4273545, -12555558, -29365436, -5498272 }, + { 17510331, -322857, 5854289, 8403524, 17133918, -3112612, -28111007, 12327945, 10750447, 10014012 }, + { -10312768, 3936952, 9156313, -8897683, 16498692, -994647, -27481051, -666732, 3424691, 7540221 } + }, + { + { 30322361, -6964110, 11361005, -4143317, 7433304, 4989748, -7071422, -16317219, -9244265, 15258046 }, + { 13054562, -2779497, 19155474, 469045, -12482797, 4566042, 5631406, 2711395, 1062915, -5136345 }, + { -19240248, -11254599, -29509029, -7499965, -5835763, 13005411, -6066489, 12194497, 32960380, 1459310 } + } +}, +{ /* 29/31 */ + { + { 19852034, 7027924, 23669353, 10020366, 8586503, -6657907, 394197, -6101885, 18638003, -11174937 }, + { 31395534, 15098109, 26581030, 8030562, -16527914, -5007134, 9012486, -7584354, -6643087, -5442636 }, + { -9192165, -2347377, -1997099, 4529534, 25766844, 607986, -13222, 9677543, -32294889, -6456008 } + }, + { + { -2444496, -149937, 29348902, 8186665, 1873760, 12489863, -30934579, -7839692, -7852844, -8138429 }, + { -15236356, -15433509, 7766470, 746860, 26346930, -10221762, -27333451, 10754588, -9431476, 5203576 }, + { 31834314, 14135496, -770007, 5159118, 20917671, -16768096, -7467973, -7337524, 31809243, 7347066 } + }, + { + { -9606723, -11874240, 20414459, 13033986, 13716524, -11691881, 19797970, -12211255, 15192876, -2087490 }, + { -12663563, -2181719, 1168162, -3804809, 26747877, -14138091, 10609330, 12694420, 33473243, -13382104 }, + { 33184999, 11180355, 15832085, -11385430, -1633671, 225884, 15089336, -11023903, -6135662, 14480053 } + }, + { + { 31308717, -5619998, 31030840, -1897099, 15674547, -6582883, 5496208, 13685227, 27595050, 8737275 }, + { -20318852, -15150239, 10933843, -16178022, 8335352, -7546022, -31008351, -12610604, 26498114, 66511 }, + { 22644454, -8761729, -16671776, 4884562, -3105614, -13559366, 30540766, -4286747, -13327787, -7515095 } + }, + { + { -28017847, 9834845, 18617207, -2681312, -3401956, -13307506, 8205540, 13585437, -17127465, 15115439 }, + { 23711543, -672915, 31206561, -8362711, 6164647, -9709987, -33535882, -1426096, 8236921, 16492939 }, + { -23910559, -13515526, -26299483, -4503841, 25005590, -7687270, 19574902, 10071562, 6708380, -6222424 } + }, + { + { 2101391, -4930054, 19702731, 2367575, -15427167, 1047675, 5301017, 9328700, 29955601, -11678310 }, + { 3096359, 9271816, -21620864, -15521844, -14847996, -7592937, -25892142, -12635595, -9917575, 6216608 }, + { -32615849, 338663, -25195611, 2510422, -29213566, -13820213, 24822830, -6146567, -26767480, 7525079 } + }, + { + { -23066649, -13985623, 16133487, -7896178, -3389565, 778788, -910336, -2782495, -19386633, 11994101 }, + { 21691500, -13624626, -641331, -14367021, 3285881, -3483596, -25064666, 9718258, -7477437, 13381418 }, + { 18445390, -4202236, 14979846, 11622458, -1727110, -3582980, 23111648, -6375247, 28535282, 15779576 } + }, + { + { 30098053, 3089662, -9234387, 16662135, -21306940, 11308411, -14068454, 12021730, 9955285, -16303356 }, + { 9734894, -14576830, -7473633, -9138735, 2060392, 11313496, -18426029, 9924399, 20194861, 13380996 }, + { -26378102, -7965207, -22167821, 15789297, -18055342, -6168792, -1984914, 15707771, 26342023, 10146099 } + } +}, +{ /* 30/31 */ + { + { -26016874, -219943, 21339191, -41388, 19745256, -2878700, -29637280, 2227040, 21612326, -545728 }, + { -13077387, 1184228, 23562814, -5970442, -20351244, -6348714, 25764461, 12243797, -20856566, 11649658 }, + { -10031494, 11262626, 27384172, 2271902, 26947504, -15997771, 39944, 6114064, 33514190, 2333242 } + }, + { + { -21433588, -12421821, 8119782, 7219913, -21830522, -9016134, -6679750, -12670638, 24350578, -13450001 }, + { -4116307, -11271533, -23886186, 4843615, -30088339, 690623, -31536088, -10406836, 8317860, 12352766 }, + { 18200138, -14475911, -33087759, -2696619, -23702521, -9102511, -23552096, -2287550, 20712163, 6719373 } + }, + { + { 26656208, 6075253, -7858556, 1886072, -28344043, 4262326, 11117530, -3763210, 26224235, -3297458 }, + { -17168938, -14854097, -3395676, -16369877, -19954045, 14050420, 21728352, 9493610, 18620611, -16428628 }, + { -13323321, 13325349, 11432106, 5964811, 18609221, 6062965, -5269471, -9725556, -30701573, -16479657 } + }, + { + { -23860538, -11233159, 26961357, 1640861, -32413112, -16737940, 12248509, -5240639, 13735342, 1934062 }, + { 25089769, 6742589, 17081145, -13406266, 21909293, -16067981, -15136294, -3765346, -21277997, 5473616 }, + { 31883677, -7961101, 1083432, -11572403, 22828471, 13290673, -7125085, 12469656, 29111212, -5451014 } + }, + { + { 24244947, -15050407, -26262976, 2791540, -14997599, 16666678, 24367466, 6388839, -10295587, 452383 }, + { -25640782, -3417841, 5217916, 16224624, 19987036, -4082269, -24236251, -5915248, 15766062, 8407814 }, + { -20406999, 13990231, 15495425, 16395525, 5377168, 15166495, -8917023, -4388953, -8067909, 2276718 } + }, + { + { 30157918, 12924066, -17712050, 9245753, 19895028, 3368142, -23827587, 5096219, 22740376, -7303417 }, + { 2041139, -14256350, 7783687, 13876377, -25946985, -13352459, 24051124, 13742383, -15637599, 13295222 }, + { 33338237, -8505733, 12532113, 7977527, 9106186, -1715251, -17720195, -4612972, -4451357, -14669444 } + }, + { + { -20045281, 5454097, -14346548, 6447146, 28862071, 1883651, -2469266, -4141880, 7770569, 9620597 }, + { 23208068, 7979712, 33071466, 8149229, 1758231, -10834995, 30945528, -1694323, -33502340, -14767970 }, + { 1439958, -16270480, -1079989, -793782, 4625402, 10647766, -5043801, 1220118, 30494170, -11440799 } + }, + { + { -5037580, -13028295, -2970559, -3061767, 15640974, -6701666, -26739026, 926050, -1684339, -13333647 }, + { 13908495, -3549272, 30919928, -6273825, -21521863, 7989039, 9021034, 9078865, 3353509, 4033511 }, + { -29663431, -15113610, 32259991, -344482, 24295849, -12912123, 23161163, 8839127, 27485041, 7356032 } + } +}, +{ /* 31/31 */ + { + { 9661027, 705443, 11980065, -5370154, -1628543, 14661173, -6346142, 2625015, 28431036, -16771834 }, + { -23839233, -8311415, -25945511, 7480958, -17681669, -8354183, -22545972, 14150565, 15970762, 4099461 }, + { 29262576, 16756590, 26350592, -8793563, 8529671, -11208050, 13617293, -9937143, 11465739, 8317062 } + }, + { + { -25493081, -6962928, 32500200, -9419051, -23038724, -2302222, 14898637, 3848455, 20969334, -5157516 }, + { -20384450, -14347713, -18336405, 13884722, -33039454, 2842114, -21610826, -3649888, 11177095, 14989547 }, + { -24496721, -11716016, 16959896, 2278463, 12066309, 10137771, 13515641, 2581286, -28487508, 9930240 } + }, + { + { -17751622, -2097826, 16544300, -13009300, -15914807, -14949081, 18345767, -13403753, 16291481, -5314038 }, + { -33229194, 2553288, 32678213, 9875984, 8534129, 6889387, -9676774, 6957617, 4368891, 9788741 }, + { 16660756, 7281060, -10830758, 12911820, 20108584, -8101676, -21722536, -8613148, 16250552, -11111103 } + }, + { + { -19765507, 2390526, -16551031, 14161980, 1905286, 6414907, 4689584, 10604807, -30190403, 4782747 }, + { -1354539, 14736941, -7367442, -13292886, 7710542, -14155590, -9981571, 4383045, 22546403, 437323 }, + { 31665577, -12180464, -16186830, 1491339, -18368625, 3294682, 27343084, 2786261, -30633590, -14097016 } + }, + { + { -14467279, -683715, -33374107, 7448552, 19294360, 14334329, -19690631, 2355319, -19284671, -6114373 }, + { 15121312, -15796162, 6377020, -6031361, -10798111, -12957845, 18952177, 15496498, -29380133, 11754228 }, + { -2637277, -13483075, 8488727, -14303896, 12728761, -1622493, 7141596, 11724556, 22761615, -10134141 } + }, + { + { 16918416, 11729663, -18083579, 3022987, -31015732, -13339659, -28741185, -12227393, 32851222, 11717399 }, + { 11166634, 7338049, -6722523, 4531520, -29468672, -7302055, 31474879, 3483633, -1193175, -4030831 }, + { -185635, 9921305, 31456609, -13536438, -12013818, 13348923, 33142652, 6546660, -19985279, -3948376 } + }, + { + { -32460596, 11266712, -11197107, -7899103, 31703694, 3855903, -8537131, -12833048, -30772034, -15486313 }, + { -18006477, 12709068, 3991746, -6479188, -21491523, -10550425, -31135347, -16049879, 10928917, 3011958 }, + { -6957757, -15594337, 31696059, 334240, 29576716, 14796075, -30831056, -12805180, 18008031, 10258577 } + }, + { + { -22448644, 15655569, 7018479, -4410003, -30314266, -1201591, -1853465, 1367120, 25127874, 6671743 }, + { 29701166, -14373934, -10878120, 9279288, -17568, 13127210, 21382910, 11042292, 25838796, 4642684 }, + { -20430234, 14955537, -24126347, 8124619, -5369288, -5990470, 30468147, -13900640, 18423289, 4177476 } + } +} diff --git a/packages/runtime/vendor/libsodium/fe_25_5/base2.h b/packages/runtime/vendor/libsodium/fe_25_5/base2.h new file mode 100644 index 0000000000..90a1457eab --- /dev/null +++ b/packages/runtime/vendor/libsodium/fe_25_5/base2.h @@ -0,0 +1,40 @@ +{ + { 25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605 }, + { -12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378 }, + { -8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546 } +}, +{ + { 15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024 }, + { 16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574 }, + { 30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357 } +}, +{ + { 10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380 }, + { 4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306 }, + { 19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942 } +}, +{ + { 5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766 }, + { -30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701 }, + { 28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300 } +}, +{ + { -22518993, -6692182, 14201702, -8745502, -23510406, 8844726, 18474211, -1361450, -13062696, 13821877 }, + { -6455177, -7839871, 3374702, -4740862, -27098617, -10571707, 31655028, -7212327, 18853322, -14220951 }, + { 4566830, -12963868, -28974889, -12240689, -7602672, -2830569, -8514358, -10431137, 2207753, -3209784 } +}, +{ + { -25154831, -4185821, 29681144, 7868801, -6854661, -9423865, -12437364, -663000, -31111463, -16132436 }, + { 25576264, -2703214, 7349804, -11814844, 16472782, 9300885, 3844789, 15725684, 171356, 6466918 }, + { 23103977, 13316479, 9739013, -16149481, 817875, -15038942, 8965339, -14088058, -30714912, 16193877 } +}, +{ + { -33521811, 3180713, -2394130, 14003687, -16903474, -16270840, 17238398, 4729455, -18074513, 9256800 }, + { -25182317, -4174131, 32336398, 5036987, -21236817, 11360617, 22616405, 9761698, -19827198, 630305 }, + { -13720693, 2639453, -24237460, -7406481, 9494427, -5774029, -6554551, -15960994, -2449256, -14291300 } +}, +{ + { -3151181, -5046075, 9282714, 6866145, -31907062, -863023, -18940575, 15033784, 25105118, -7894876 }, + { -24326370, 15950226, -31801215, -14592823, -11662737, -5090925, 1573892, -2625887, 2198790, -15804619 }, + { -3099351, 10324967, -2241613, 7453183, -5446979, -2735503, -13812022, -16236442, -32461234, -12290683 } +} diff --git a/packages/runtime/vendor/libsodium/fe_25_5/constants.h b/packages/runtime/vendor/libsodium/fe_25_5/constants.h new file mode 100644 index 0000000000..dd6da1b874 --- /dev/null +++ b/packages/runtime/vendor/libsodium/fe_25_5/constants.h @@ -0,0 +1,40 @@ +/* 37095705934669439343138083508754565189542113879843219016388785533085940283555 */ +static const fe25519 d = { + -10913610, 13857413, -15372611, 6949391, 114729, -8787816, -6275908, -3247719, -18696448, -12055116 +}; + +/* 2 * d = + * 16295367250680780974490674513165176452449235426866156013048779062215315747161 + */ +static const fe25519 d2 = { + -21827239, -5839606, -30745221, 13898782, 229458, 15978800, -12551817, -6495438, 29715968, 9444199 }; + +/* sqrt(-1) */ +static const fe25519 sqrtm1 = { + -32595792, -7943725, 9377950, 3500415, 12389472, -272473, -25146209, -2005654, 326686, 11406482 +}; + +/* A = 486662 */ +static const fe25519 curve25519_A = { + 486662, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +/* sqrt(ad - 1) with a = -1 (mod p) */ +static const fe25519 sqrtadm1 = { + 24849947, -153582, -23613485, 6347715, -21072328, -667138, -25271143, -15367704, -870347, 14525639 +}; + +/* 1 / sqrt(a - d) */ +static const fe25519 invsqrtamd = { + 6111485, 4156064, -27798727, 12243468, -25904040, 120897, 20826367, -7060776, 6093568, -1986012 +}; + +/* 1 - d ^ 2 */ +static const fe25519 onemsqd = { + 6275446, -16617371, -22938544, -3773710, 11667077, 7397348, -27922721, 1766195, -24433858, 672203 +}; + +/* (d - 1) ^ 2 */ +static const fe25519 sqdmone = { + 15551795, -11097455, -13425098, -10125071, -11896535, 10178284, -26634327, 4729244, -5282110, -10116402 +}; diff --git a/packages/runtime/vendor/libsodium/fe_25_5/fe.h b/packages/runtime/vendor/libsodium/fe_25_5/fe.h new file mode 100644 index 0000000000..9e11afd114 --- /dev/null +++ b/packages/runtime/vendor/libsodium/fe_25_5/fe.h @@ -0,0 +1,220 @@ +/* + Ignores top bit of s. + */ + +void +fe25519_frombytes(fe25519 h, const unsigned char *s) +{ + int64_t h0 = load_4(s); + int64_t h1 = load_3(s + 4) << 6; + int64_t h2 = load_3(s + 7) << 5; + int64_t h3 = load_3(s + 10) << 3; + int64_t h4 = load_3(s + 13) << 2; + int64_t h5 = load_4(s + 16); + int64_t h6 = load_3(s + 20) << 7; + int64_t h7 = load_3(s + 23) << 5; + int64_t h8 = load_3(s + 26) << 4; + int64_t h9 = (load_3(s + 29) & 8388607) << 2; + + int64_t carry0; + int64_t carry1; + int64_t carry2; + int64_t carry3; + int64_t carry4; + int64_t carry5; + int64_t carry6; + int64_t carry7; + int64_t carry8; + int64_t carry9; + + carry9 = (h9 + (int64_t)(1L << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 * ((uint64_t) 1L << 25); + carry1 = (h1 + (int64_t)(1L << 24)) >> 25; + h2 += carry1; + h1 -= carry1 * ((uint64_t) 1L << 25); + carry3 = (h3 + (int64_t)(1L << 24)) >> 25; + h4 += carry3; + h3 -= carry3 * ((uint64_t) 1L << 25); + carry5 = (h5 + (int64_t)(1L << 24)) >> 25; + h6 += carry5; + h5 -= carry5 * ((uint64_t) 1L << 25); + carry7 = (h7 + (int64_t)(1L << 24)) >> 25; + h8 += carry7; + h7 -= carry7 * ((uint64_t) 1L << 25); + + carry0 = (h0 + (int64_t)(1L << 25)) >> 26; + h1 += carry0; + h0 -= carry0 * ((uint64_t) 1L << 26); + carry2 = (h2 + (int64_t)(1L << 25)) >> 26; + h3 += carry2; + h2 -= carry2 * ((uint64_t) 1L << 26); + carry4 = (h4 + (int64_t)(1L << 25)) >> 26; + h5 += carry4; + h4 -= carry4 * ((uint64_t) 1L << 26); + carry6 = (h6 + (int64_t)(1L << 25)) >> 26; + h7 += carry6; + h6 -= carry6 * ((uint64_t) 1L << 26); + carry8 = (h8 + (int64_t)(1L << 25)) >> 26; + h9 += carry8; + h8 -= carry8 * ((uint64_t) 1L << 26); + + h[0] = (int32_t) h0; + h[1] = (int32_t) h1; + h[2] = (int32_t) h2; + h[3] = (int32_t) h3; + h[4] = (int32_t) h4; + h[5] = (int32_t) h5; + h[6] = (int32_t) h6; + h[7] = (int32_t) h7; + h[8] = (int32_t) h8; + h[9] = (int32_t) h9; +} + +/* + Preconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + + Write p=2^255-19; q=floor(h/p). + Basic claim: q = floor(2^(-255)(h + 19 2^(-25)h9 + 2^(-1))). + + Proof: + Have |h|<=p so |q|<=1 so |19^2 2^(-255) q|<1/4. + Also have |h-2^230 h9|<2^231 so |19 2^(-255)(h-2^230 h9)|<1/4. + + Write y=2^(-1)-19^2 2^(-255)q-19 2^(-255)(h-2^230 h9). + Then 0> 25; + q = (h0 + q) >> 26; + q = (h1 + q) >> 25; + q = (h2 + q) >> 26; + q = (h3 + q) >> 25; + q = (h4 + q) >> 26; + q = (h5 + q) >> 25; + q = (h6 + q) >> 26; + q = (h7 + q) >> 25; + q = (h8 + q) >> 26; + q = (h9 + q) >> 25; + + /* Goal: Output h-(2^255-19)q, which is between 0 and 2^255-20. */ + h0 += 19 * q; + /* Goal: Output h-2^255 q, which is between 0 and 2^255-20. */ + + carry0 = h0 >> 26; + h1 += carry0; + h0 -= carry0 * ((uint32_t) 1L << 26); + carry1 = h1 >> 25; + h2 += carry1; + h1 -= carry1 * ((uint32_t) 1L << 25); + carry2 = h2 >> 26; + h3 += carry2; + h2 -= carry2 * ((uint32_t) 1L << 26); + carry3 = h3 >> 25; + h4 += carry3; + h3 -= carry3 * ((uint32_t) 1L << 25); + carry4 = h4 >> 26; + h5 += carry4; + h4 -= carry4 * ((uint32_t) 1L << 26); + carry5 = h5 >> 25; + h6 += carry5; + h5 -= carry5 * ((uint32_t) 1L << 25); + carry6 = h6 >> 26; + h7 += carry6; + h6 -= carry6 * ((uint32_t) 1L << 26); + carry7 = h7 >> 25; + h8 += carry7; + h7 -= carry7 * ((uint32_t) 1L << 25); + carry8 = h8 >> 26; + h9 += carry8; + h8 -= carry8 * ((uint32_t) 1L << 26); + carry9 = h9 >> 25; + h9 -= carry9 * ((uint32_t) 1L << 25); + + h[0] = h0; + h[1] = h1; + h[2] = h2; + h[3] = h3; + h[4] = h4; + h[5] = h5; + h[6] = h6; + h[7] = h7; + h[8] = h8; + h[9] = h9; +} + +/* + Goal: Output h0+...+2^255 h10-2^255 q, which is between 0 and 2^255-20. + Have h0+...+2^230 h9 between 0 and 2^255-1; + evidently 2^255 h10-2^255 q = 0. + + Goal: Output h0+...+2^230 h9. + */ + +void +fe25519_tobytes(unsigned char *s, const fe25519 h) +{ + fe25519 t; + + fe25519_reduce(t, h); + s[0] = t[0] >> 0; + s[1] = t[0] >> 8; + s[2] = t[0] >> 16; + s[3] = (t[0] >> 24) | (t[1] * ((uint32_t) 1 << 2)); + s[4] = t[1] >> 6; + s[5] = t[1] >> 14; + s[6] = (t[1] >> 22) | (t[2] * ((uint32_t) 1 << 3)); + s[7] = t[2] >> 5; + s[8] = t[2] >> 13; + s[9] = (t[2] >> 21) | (t[3] * ((uint32_t) 1 << 5)); + s[10] = t[3] >> 3; + s[11] = t[3] >> 11; + s[12] = (t[3] >> 19) | (t[4] * ((uint32_t) 1 << 6)); + s[13] = t[4] >> 2; + s[14] = t[4] >> 10; + s[15] = t[4] >> 18; + s[16] = t[5] >> 0; + s[17] = t[5] >> 8; + s[18] = t[5] >> 16; + s[19] = (t[5] >> 24) | (t[6] * ((uint32_t) 1 << 1)); + s[20] = t[6] >> 7; + s[21] = t[6] >> 15; + s[22] = (t[6] >> 23) | (t[7] * ((uint32_t) 1 << 3)); + s[23] = t[7] >> 5; + s[24] = t[7] >> 13; + s[25] = (t[7] >> 21) | (t[8] * ((uint32_t) 1 << 4)); + s[26] = t[8] >> 4; + s[27] = t[8] >> 12; + s[28] = (t[8] >> 20) | (t[9] * ((uint32_t) 1 << 6)); + s[29] = t[9] >> 2; + s[30] = t[9] >> 10; + s[31] = t[9] >> 18; +} diff --git a/packages/runtime/vendor/libsodium/hash_sha512_cp.c b/packages/runtime/vendor/libsodium/hash_sha512_cp.c new file mode 100644 index 0000000000..59a8b93686 --- /dev/null +++ b/packages/runtime/vendor/libsodium/hash_sha512_cp.c @@ -0,0 +1,284 @@ + +/*- + * Copyright 2005,2007,2009 Colin Percival + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + */ + +#include +#include +#include +#include + +#include + +#include "crypto_hash_sha512.h" +#include "common.h" +#include "utils.h" + +static void +be64enc_vect(unsigned char *dst, const uint64_t *src, size_t len) +{ + size_t i; + + for (i = 0; i < len / 8; i++) { + STORE64_BE(dst + i * 8, src[i]); + } +} + +static void +be64dec_vect(uint64_t *dst, const unsigned char *src, size_t len) +{ + size_t i; + + for (i = 0; i < len / 8; i++) { + dst[i] = LOAD64_BE(src + i * 8); + } +} + +static const uint64_t Krnd[80] = { + 0x428a2f98d728ae22ULL, 0x7137449123ef65cdULL, 0xb5c0fbcfec4d3b2fULL, + 0xe9b5dba58189dbbcULL, 0x3956c25bf348b538ULL, 0x59f111f1b605d019ULL, + 0x923f82a4af194f9bULL, 0xab1c5ed5da6d8118ULL, 0xd807aa98a3030242ULL, + 0x12835b0145706fbeULL, 0x243185be4ee4b28cULL, 0x550c7dc3d5ffb4e2ULL, + 0x72be5d74f27b896fULL, 0x80deb1fe3b1696b1ULL, 0x9bdc06a725c71235ULL, + 0xc19bf174cf692694ULL, 0xe49b69c19ef14ad2ULL, 0xefbe4786384f25e3ULL, + 0x0fc19dc68b8cd5b5ULL, 0x240ca1cc77ac9c65ULL, 0x2de92c6f592b0275ULL, + 0x4a7484aa6ea6e483ULL, 0x5cb0a9dcbd41fbd4ULL, 0x76f988da831153b5ULL, + 0x983e5152ee66dfabULL, 0xa831c66d2db43210ULL, 0xb00327c898fb213fULL, + 0xbf597fc7beef0ee4ULL, 0xc6e00bf33da88fc2ULL, 0xd5a79147930aa725ULL, + 0x06ca6351e003826fULL, 0x142929670a0e6e70ULL, 0x27b70a8546d22ffcULL, + 0x2e1b21385c26c926ULL, 0x4d2c6dfc5ac42aedULL, 0x53380d139d95b3dfULL, + 0x650a73548baf63deULL, 0x766a0abb3c77b2a8ULL, 0x81c2c92e47edaee6ULL, + 0x92722c851482353bULL, 0xa2bfe8a14cf10364ULL, 0xa81a664bbc423001ULL, + 0xc24b8b70d0f89791ULL, 0xc76c51a30654be30ULL, 0xd192e819d6ef5218ULL, + 0xd69906245565a910ULL, 0xf40e35855771202aULL, 0x106aa07032bbd1b8ULL, + 0x19a4c116b8d2d0c8ULL, 0x1e376c085141ab53ULL, 0x2748774cdf8eeb99ULL, + 0x34b0bcb5e19b48a8ULL, 0x391c0cb3c5c95a63ULL, 0x4ed8aa4ae3418acbULL, + 0x5b9cca4f7763e373ULL, 0x682e6ff3d6b2b8a3ULL, 0x748f82ee5defb2fcULL, + 0x78a5636f43172f60ULL, 0x84c87814a1f0ab72ULL, 0x8cc702081a6439ecULL, + 0x90befffa23631e28ULL, 0xa4506cebde82bde9ULL, 0xbef9a3f7b2c67915ULL, + 0xc67178f2e372532bULL, 0xca273eceea26619cULL, 0xd186b8c721c0c207ULL, + 0xeada7dd6cde0eb1eULL, 0xf57d4f7fee6ed178ULL, 0x06f067aa72176fbaULL, + 0x0a637dc5a2c898a6ULL, 0x113f9804bef90daeULL, 0x1b710b35131c471bULL, + 0x28db77f523047d84ULL, 0x32caab7b40c72493ULL, 0x3c9ebe0a15c9bebcULL, + 0x431d67c49c100d4cULL, 0x4cc5d4becb3e42b6ULL, 0x597f299cfc657e2aULL, + 0x5fcb6fab3ad6faecULL, 0x6c44198c4a475817ULL +}; + +#define Ch(x, y, z) ((x & (y ^ z)) ^ z) +#define Maj(x, y, z) ((x & (y | z)) | (y & z)) +#define SHR(x, n) (x >> n) +#define ROTR(x, n) ROTR64(x, n) +#define S0(x) (ROTR(x, 28) ^ ROTR(x, 34) ^ ROTR(x, 39)) +#define S1(x) (ROTR(x, 14) ^ ROTR(x, 18) ^ ROTR(x, 41)) +#define s0(x) (ROTR(x, 1) ^ ROTR(x, 8) ^ SHR(x, 7)) +#define s1(x) (ROTR(x, 19) ^ ROTR(x, 61) ^ SHR(x, 6)) + +#define RND(a, b, c, d, e, f, g, h, k) \ + h += S1(e) + Ch(e, f, g) + k; \ + d += h; \ + h += S0(a) + Maj(a, b, c); + +#define RNDr(S, W, i, ii) \ + RND(S[(80 - i) % 8], S[(81 - i) % 8], S[(82 - i) % 8], S[(83 - i) % 8], \ + S[(84 - i) % 8], S[(85 - i) % 8], S[(86 - i) % 8], S[(87 - i) % 8], \ + W[i + ii] + Krnd[i + ii]) + +#define MSCH(W, ii, i) \ + W[i + ii + 16] = \ + s1(W[i + ii + 14]) + W[i + ii + 9] + s0(W[i + ii + 1]) + W[i + ii] + +static void +SHA512_Transform(uint64_t *state, const uint8_t block[128], uint64_t W[80], + uint64_t S[8]) +{ + int i; + + be64dec_vect(W, block, 128); + memcpy(S, state, 64); + for (i = 0; i < 80; i += 16) { + RNDr(S, W, 0, i); + RNDr(S, W, 1, i); + RNDr(S, W, 2, i); + RNDr(S, W, 3, i); + RNDr(S, W, 4, i); + RNDr(S, W, 5, i); + RNDr(S, W, 6, i); + RNDr(S, W, 7, i); + RNDr(S, W, 8, i); + RNDr(S, W, 9, i); + RNDr(S, W, 10, i); + RNDr(S, W, 11, i); + RNDr(S, W, 12, i); + RNDr(S, W, 13, i); + RNDr(S, W, 14, i); + RNDr(S, W, 15, i); + if (i == 64) { + break; + } + MSCH(W, 0, i); + MSCH(W, 1, i); + MSCH(W, 2, i); + MSCH(W, 3, i); + MSCH(W, 4, i); + MSCH(W, 5, i); + MSCH(W, 6, i); + MSCH(W, 7, i); + MSCH(W, 8, i); + MSCH(W, 9, i); + MSCH(W, 10, i); + MSCH(W, 11, i); + MSCH(W, 12, i); + MSCH(W, 13, i); + MSCH(W, 14, i); + MSCH(W, 15, i); + } + for (i = 0; i < 8; i++) { + state[i] += S[i]; + } +} + +static const uint8_t PAD[128] = { + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +static void +SHA512_Pad(crypto_hash_sha512_state *state, uint64_t tmp64[80 + 8]) +{ + unsigned int r; + unsigned int i; + + ACQUIRE_FENCE; + r = (unsigned int) ((state->count[1] >> 3) & 0x7f); + if (r < 112) { + for (i = 0; i < 112 - r; i++) { + state->buf[r + i] = PAD[i]; + } + } else { + for (i = 0; i < 128 - r; i++) { + state->buf[r + i] = PAD[i]; + } + SHA512_Transform(state->state, state->buf, &tmp64[0], &tmp64[80]); + memset(&state->buf[0], 0, 112); + } + be64enc_vect(&state->buf[112], state->count, 16); + SHA512_Transform(state->state, state->buf, &tmp64[0], &tmp64[80]); +} + +int +crypto_hash_sha512_init(crypto_hash_sha512_state *state) +{ + static const uint64_t sha512_initial_state[8] = { + 0x6a09e667f3bcc908ULL, 0xbb67ae8584caa73bULL, 0x3c6ef372fe94f82bULL, + 0xa54ff53a5f1d36f1ULL, 0x510e527fade682d1ULL, 0x9b05688c2b3e6c1fULL, + 0x1f83d9abfb41bd6bULL, 0x5be0cd19137e2179ULL + }; + + state->count[0] = state->count[1] = (uint64_t) 0U; + memcpy(state->state, sha512_initial_state, sizeof sha512_initial_state); + + return 0; +} + +int +crypto_hash_sha512_update(crypto_hash_sha512_state *state, + const unsigned char *in, unsigned long long inlen) +{ + uint64_t tmp64[80 + 8]; + uint64_t bitlen[2]; + unsigned long long i; + unsigned long long r; + + if (inlen <= 0U) { + return 0; + } + ACQUIRE_FENCE; + r = (unsigned long long) ((state->count[1] >> 3) & 0x7f); + + bitlen[1] = ((uint64_t) inlen) << 3; + bitlen[0] = ((uint64_t) inlen) >> 61; + /* LCOV_EXCL_START */ + if ((state->count[1] += bitlen[1]) < bitlen[1]) { + state->count[0]++; + } + /* LCOV_EXCL_STOP */ + state->count[0] += bitlen[0]; + if (inlen < 128 - r) { + for (i = 0; i < inlen; i++) { + state->buf[r + i] = in[i]; + } + return 0; + } + for (i = 0; i < 128 - r; i++) { + state->buf[r + i] = in[i]; + } + SHA512_Transform(state->state, state->buf, &tmp64[0], &tmp64[80]); + in += 128 - r; + inlen -= 128 - r; + + while (inlen >= 128) { + SHA512_Transform(state->state, in, &tmp64[0], &tmp64[80]); + in += 128; + inlen -= 128; + } + inlen &= 127; + for (i = 0; i < inlen; i++) { + state->buf[i] = in[i]; + } + sodium_memzero((void *) tmp64, sizeof tmp64); + + return 0; +} + +int +crypto_hash_sha512_final(crypto_hash_sha512_state *state, unsigned char *out) +{ + uint64_t tmp64[80 + 8]; + + SHA512_Pad(state, tmp64); + be64enc_vect(out, state->state, 64); + sodium_memzero((void *) tmp64, sizeof tmp64); + sodium_memzero((void *) state, sizeof *state); + + return 0; +} + +int +crypto_hash_sha512(unsigned char *out, const unsigned char *in, + unsigned long long inlen) +{ + crypto_hash_sha512_state state; + + crypto_hash_sha512_init(&state); + crypto_hash_sha512_update(&state, in, inlen); + crypto_hash_sha512_final(&state, out); + + return 0; +} diff --git a/packages/runtime/vendor/libsodium/keypair.c b/packages/runtime/vendor/libsodium/keypair.c new file mode 100644 index 0000000000..18059adfe1 --- /dev/null +++ b/packages/runtime/vendor/libsodium/keypair.c @@ -0,0 +1,84 @@ + +#include + +#include "crypto_hash_sha512.h" +#include "crypto_scalarmult_curve25519.h" +#include "crypto_sign_ed25519.h" +#include "sign_ed25519_ref10.h" +#include "ed25519_ref10.h" +#include "randombytes.h" +#include "utils.h" + +int +crypto_sign_ed25519_seed_keypair(unsigned char *pk, unsigned char *sk, + const unsigned char *seed) +{ + ge25519_p3 A; + + crypto_hash_sha512(sk, seed, 32); + sk[0] &= 248; + sk[31] &= 127; + sk[31] |= 64; + + ge25519_scalarmult_base(&A, sk); + ge25519_p3_tobytes(pk, &A); + + memmove(sk, seed, 32); + memmove(sk + 32, pk, 32); + + return 0; +} + +int +crypto_sign_ed25519_keypair(unsigned char *pk, unsigned char *sk) +{ + unsigned char seed[32]; + int ret; + + randombytes_buf(seed, sizeof seed); + ret = crypto_sign_ed25519_seed_keypair(pk, sk, seed); + sodium_memzero(seed, sizeof seed); + + return ret; +} + +int +crypto_sign_ed25519_pk_to_curve25519(unsigned char *curve25519_pk, + const unsigned char *ed25519_pk) +{ + ge25519_p3 A; + fe25519 x; + fe25519 one_minus_y; + + if (ge25519_has_small_order(ed25519_pk) != 0 || + ge25519_frombytes_negate_vartime(&A, ed25519_pk) != 0 || + ge25519_is_on_main_subgroup(&A) == 0) { + return -1; + } + fe25519_1(one_minus_y); + /* assumes A.Z=1 */ + fe25519_sub(one_minus_y, one_minus_y, A.Y); + fe25519_1(x); + fe25519_add(x, x, A.Y); + fe25519_invert(one_minus_y, one_minus_y); + fe25519_mul(x, x, one_minus_y); + fe25519_tobytes(curve25519_pk, x); + + return 0; +} + +int +crypto_sign_ed25519_sk_to_curve25519(unsigned char *curve25519_sk, + const unsigned char *ed25519_sk) +{ + unsigned char h[crypto_hash_sha512_BYTES]; + + crypto_hash_sha512(h, ed25519_sk, 32); + h[0] &= 248; + h[31] &= 127; + h[31] |= 64; + memcpy(curve25519_sk, h, crypto_scalarmult_curve25519_BYTES); + sodium_memzero(h, sizeof h); + + return 0; +} diff --git a/packages/runtime/vendor/libsodium/open.c b/packages/runtime/vendor/libsodium/open.c new file mode 100644 index 0000000000..0670866d68 --- /dev/null +++ b/packages/runtime/vendor/libsodium/open.c @@ -0,0 +1,98 @@ + +#include +#include +#include + +#include "crypto_hash_sha512.h" +#include "crypto_sign_ed25519.h" +#include "crypto_verify_32.h" +#include "sign_ed25519_ref10.h" +#include "common.h" +#include "ed25519_ref10.h" +#include "utils.h" + +int +_crypto_sign_ed25519_verify_detached(const unsigned char *sig, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *pk, + int prehashed) +{ + crypto_hash_sha512_state hs; + unsigned char h[64]; + unsigned char rcheck[32]; + ge25519_p3 A; + ge25519_p2 R; + + ACQUIRE_FENCE; +#ifdef ED25519_COMPAT + if (sig[63] & 224) { + return -1; + } +#else + if (sc25519_is_canonical(sig + 32) == 0 || + ge25519_has_small_order(sig) != 0) { + return -1; + } + if (ge25519_is_canonical(pk) == 0 || + ge25519_has_small_order(pk) != 0) { + return -1; + } +#endif + if (ge25519_frombytes_negate_vartime(&A, pk) != 0) { + return -1; + } + _crypto_sign_ed25519_ref10_hinit(&hs, prehashed); + crypto_hash_sha512_update(&hs, sig, 32); + crypto_hash_sha512_update(&hs, pk, 32); + crypto_hash_sha512_update(&hs, m, mlen); + crypto_hash_sha512_final(&hs, h); + sc25519_reduce(h); + + ge25519_double_scalarmult_vartime(&R, h, &A, sig + 32); + ge25519_tobytes(rcheck, &R); + + return crypto_verify_32(rcheck, sig) | (-(rcheck == sig)) | + sodium_memcmp(sig, rcheck, 32); +} + +int +crypto_sign_ed25519_verify_detached(const unsigned char *sig, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *pk) +{ + return _crypto_sign_ed25519_verify_detached(sig, m, mlen, pk, 0); +} + +int +crypto_sign_ed25519_open(unsigned char *m, unsigned long long *mlen_p, + const unsigned char *sm, unsigned long long smlen, + const unsigned char *pk) +{ + unsigned long long mlen; + + if (smlen < 64 || smlen - 64 > crypto_sign_ed25519_MESSAGEBYTES_MAX) { + goto badsig; + } + mlen = smlen - 64; + if (crypto_sign_ed25519_verify_detached(sm, sm + 64, mlen, pk) != 0) { + if (m != NULL) { + memset(m, 0, mlen); + } + goto badsig; + } + if (mlen_p != NULL) { + *mlen_p = mlen; + } + if (m != NULL) { + memmove(m, sm + 64, mlen); + } + return 0; + +badsig: + if (mlen_p != NULL) { + *mlen_p = 0; + } + return -1; +} diff --git a/packages/runtime/vendor/libsodium/randombytes.h b/packages/runtime/vendor/libsodium/randombytes.h new file mode 100644 index 0000000000..c83a4df50b --- /dev/null +++ b/packages/runtime/vendor/libsodium/randombytes.h @@ -0,0 +1,72 @@ + +#ifndef randombytes_H +#define randombytes_H + +#include +#include + +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +typedef struct randombytes_implementation { + const char *(*implementation_name)(void); /* required */ + uint32_t (*random)(void); /* required */ + void (*stir)(void); /* optional */ + uint32_t (*uniform)(const uint32_t upper_bound); /* optional, a default implementation will be used if NULL */ + void (*buf)(void * const buf, const size_t size); /* required */ + int (*close)(void); /* optional */ +} randombytes_implementation; + +#define randombytes_BYTES_MAX SODIUM_MIN(SODIUM_SIZE_MAX, 0xffffffffUL) + +#define randombytes_SEEDBYTES 32U +SODIUM_EXPORT +size_t randombytes_seedbytes(void); + +SODIUM_EXPORT +void randombytes_buf(void * const buf, const size_t size) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void randombytes_buf_deterministic(void * const buf, const size_t size, + const unsigned char seed[randombytes_SEEDBYTES]) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +uint32_t randombytes_random(void); + +SODIUM_EXPORT +uint32_t randombytes_uniform(const uint32_t upper_bound); + +SODIUM_EXPORT +void randombytes_stir(void); + +SODIUM_EXPORT +int randombytes_close(void); + +SODIUM_EXPORT +int randombytes_set_implementation(const randombytes_implementation *impl) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +const char *randombytes_implementation_name(void); + +/* -- NaCl compatibility interface -- */ + +SODIUM_EXPORT +void randombytes(unsigned char * const buf, const unsigned long long buf_len) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/sign.c b/packages/runtime/vendor/libsodium/sign.c new file mode 100644 index 0000000000..1b4a2923c8 --- /dev/null +++ b/packages/runtime/vendor/libsodium/sign.c @@ -0,0 +1,128 @@ + +#include + +#include "crypto_hash_sha512.h" +#include "crypto_sign_ed25519.h" +#include "sign_ed25519_ref10.h" +#include "ed25519_ref10.h" +#include "randombytes.h" +#include "utils.h" + +void +_crypto_sign_ed25519_ref10_hinit(crypto_hash_sha512_state *hs, int prehashed) +{ + static const unsigned char DOM2PREFIX[32 + 2] = { + 'S', 'i', 'g', 'E', 'd', '2', '5', '5', '1', '9', ' ', + 'n', 'o', ' ', + 'E', 'd', '2', '5', '5', '1', '9', ' ', + 'c', 'o', 'l', 'l', 'i', 's', 'i', 'o', 'n', 's', 1, 0 + }; + + crypto_hash_sha512_init(hs); + if (prehashed) { + crypto_hash_sha512_update(hs, DOM2PREFIX, sizeof DOM2PREFIX); + } +} + +static inline void +_crypto_sign_ed25519_clamp(unsigned char k[32]) +{ + k[0] &= 248; + k[31] &= 127; + k[31] |= 64; +} + +#ifdef ED25519_NONDETERMINISTIC +/* r = hash(k || K || noise || pad || M) (mod q) */ +static void +_crypto_sign_ed25519_synthetic_r_hv(crypto_hash_sha512_state *hs, + unsigned char tmp[64], + const unsigned char az[64]) +{ + crypto_hash_sha512_update(hs, az, 64); + randombytes_buf(tmp, 32); + memset(tmp + 32, 0, 32); + crypto_hash_sha512_update(hs, tmp, 64); +} +#endif + +int +_crypto_sign_ed25519_detached(unsigned char *sig, unsigned long long *siglen_p, + const unsigned char *m, unsigned long long mlen, + const unsigned char *sk, int prehashed) +{ + crypto_hash_sha512_state hs; + unsigned char az[64]; + unsigned char nonce[64]; + unsigned char hram[64]; + ge25519_p3 R; + + _crypto_sign_ed25519_ref10_hinit(&hs, prehashed); + + crypto_hash_sha512(az, sk, 32); +#ifdef ED25519_NONDETERMINISTIC + _crypto_sign_ed25519_synthetic_r_hv(&hs, nonce /* tmp */, az); +#else + crypto_hash_sha512_update(&hs, az + 32, 32); +#endif + + crypto_hash_sha512_update(&hs, m, mlen); + crypto_hash_sha512_final(&hs, nonce); + + memmove(sig + 32, sk + 32, 32); + + sc25519_reduce(nonce); + ge25519_scalarmult_base(&R, nonce); + ge25519_p3_tobytes(sig, &R); + + _crypto_sign_ed25519_ref10_hinit(&hs, prehashed); + crypto_hash_sha512_update(&hs, sig, 64); + crypto_hash_sha512_update(&hs, m, mlen); + crypto_hash_sha512_final(&hs, hram); + + sc25519_reduce(hram); + _crypto_sign_ed25519_clamp(az); + sc25519_muladd(sig + 32, hram, az, nonce); + + sodium_memzero(az, sizeof az); + sodium_memzero(nonce, sizeof nonce); + + if (siglen_p != NULL) { + *siglen_p = 64U; + } + return 0; +} + +int +crypto_sign_ed25519_detached(unsigned char *sig, unsigned long long *siglen_p, + const unsigned char *m, unsigned long long mlen, + const unsigned char *sk) +{ + return _crypto_sign_ed25519_detached(sig, siglen_p, m, mlen, sk, 0); +} + +int +crypto_sign_ed25519(unsigned char *sm, unsigned long long *smlen_p, + const unsigned char *m, unsigned long long mlen, + const unsigned char *sk) +{ + unsigned long long siglen; + + memmove(sm + crypto_sign_ed25519_BYTES, m, mlen); + /* LCOV_EXCL_START */ + if (crypto_sign_ed25519_detached( + sm, &siglen, sm + crypto_sign_ed25519_BYTES, mlen, sk) != 0 || + siglen != crypto_sign_ed25519_BYTES) { + if (smlen_p != NULL) { + *smlen_p = 0; + } + memset(sm, 0, mlen + crypto_sign_ed25519_BYTES); + return -1; + } + /* LCOV_EXCL_STOP */ + + if (smlen_p != NULL) { + *smlen_p = mlen + siglen; + } + return 0; +} diff --git a/packages/runtime/vendor/libsodium/sign_ed25519_ref10.h b/packages/runtime/vendor/libsodium/sign_ed25519_ref10.h new file mode 100644 index 0000000000..29f45a8544 --- /dev/null +++ b/packages/runtime/vendor/libsodium/sign_ed25519_ref10.h @@ -0,0 +1,18 @@ +#ifndef sign_ed25519_ref10_H +#define sign_ed25519_ref10_H + +void _crypto_sign_ed25519_ref10_hinit(crypto_hash_sha512_state *hs, + int prehashed); + +int _crypto_sign_ed25519_detached(unsigned char *sig, + unsigned long long *siglen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *sk, int prehashed); + +int _crypto_sign_ed25519_verify_detached(const unsigned char *sig, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *pk, + int prehashed); +#endif diff --git a/packages/runtime/vendor/libsodium/utils.h b/packages/runtime/vendor/libsodium/utils.h new file mode 100644 index 0000000000..f9b369796e --- /dev/null +++ b/packages/runtime/vendor/libsodium/utils.h @@ -0,0 +1,179 @@ + +#ifndef sodium_utils_H +#define sodium_utils_H + +#include + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef SODIUM_C99 +# if defined(__cplusplus) || !defined(__STDC_VERSION__) || __STDC_VERSION__ < 199901L +# define SODIUM_C99(X) +# else +# define SODIUM_C99(X) X +# endif +#endif + +SODIUM_EXPORT +void sodium_memzero(void * const pnt, const size_t len); + +SODIUM_EXPORT +void sodium_stackzero(const size_t len); + +/* + * WARNING: sodium_memcmp() must be used to verify if two secret keys + * are equal, in constant time. + * It returns 0 if the keys are equal, and -1 if they differ. + * This function is not designed for lexicographical comparisons. + */ +SODIUM_EXPORT +int sodium_memcmp(const void * const b1_, const void * const b2_, size_t len) + __attribute__ ((warn_unused_result)); + +/* + * sodium_compare() returns -1 if b1_ < b2_, 1 if b1_ > b2_ and 0 if b1_ == b2_ + * It is suitable for lexicographical comparisons, or to compare nonces + * and counters stored in little-endian format. + * However, it is slower than sodium_memcmp(). + */ +SODIUM_EXPORT +int sodium_compare(const unsigned char *b1_, const unsigned char *b2_, + size_t len) __attribute__ ((warn_unused_result)); + +SODIUM_EXPORT +int sodium_is_zero(const unsigned char *n, const size_t nlen); + +SODIUM_EXPORT +void sodium_increment(unsigned char *n, const size_t nlen); + +SODIUM_EXPORT +void sodium_add(unsigned char *a, const unsigned char *b, const size_t len); + +SODIUM_EXPORT +void sodium_sub(unsigned char *a, const unsigned char *b, const size_t len); + +SODIUM_EXPORT +char *sodium_bin2hex(char * const hex, const size_t hex_maxlen, + const unsigned char * const bin, const size_t bin_len) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int sodium_hex2bin(unsigned char * const bin, const size_t bin_maxlen, + const char * const hex, const size_t hex_len, + const char * const ignore, size_t * const bin_len, + const char ** const hex_end) + __attribute__ ((nonnull(1))); + +#define sodium_base64_VARIANT_ORIGINAL 1 +#define sodium_base64_VARIANT_ORIGINAL_NO_PADDING 3 +#define sodium_base64_VARIANT_URLSAFE 5 +#define sodium_base64_VARIANT_URLSAFE_NO_PADDING 7 + +/* + * Computes the required length to encode BIN_LEN bytes as a base64 string + * using the given variant. The computed length includes a trailing \0. + */ +#define sodium_base64_ENCODED_LEN(BIN_LEN, VARIANT) \ + (((BIN_LEN) / 3U) * 4U + \ + ((((BIN_LEN) - ((BIN_LEN) / 3U) * 3U) | (((BIN_LEN) - ((BIN_LEN) / 3U) * 3U) >> 1)) & 1U) * \ + (4U - (~((((VARIANT) & 2U) >> 1) - 1U) & (3U - ((BIN_LEN) - ((BIN_LEN) / 3U) * 3U)))) + 1U) + +SODIUM_EXPORT +size_t sodium_base64_encoded_len(const size_t bin_len, const int variant); + +SODIUM_EXPORT +char *sodium_bin2base64(char * const b64, const size_t b64_maxlen, + const unsigned char * const bin, const size_t bin_len, + const int variant) __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int sodium_base642bin(unsigned char * const bin, const size_t bin_maxlen, + const char * const b64, const size_t b64_len, + const char * const ignore, size_t * const bin_len, + const char ** const b64_end, const int variant) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int sodium_mlock(void * const addr, const size_t len) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int sodium_munlock(void * const addr, const size_t len) + __attribute__ ((nonnull)); + +/* WARNING: sodium_malloc() and sodium_allocarray() are not general-purpose + * allocation functions. + * + * They return a pointer to a region filled with 0xd0 bytes, immediately + * followed by a guard page. + * As a result, accessing a single byte after the requested allocation size + * will intentionally trigger a segmentation fault. + * + * A canary and an additional guard page placed before the beginning of the + * region may also kill the process if a buffer underflow is detected. + * + * The memory layout is: + * [unprotected region size (read only)][guard page (no access)][unprotected pages (read/write)][guard page (no access)] + * With the layout of the unprotected pages being: + * [optional padding][16-bytes canary][user region] + * + * However: + * - These functions are significantly slower than standard functions + * - Each allocation requires 3 or 4 additional pages + * - The returned address will not be aligned if the allocation size is not + * a multiple of the required alignment. For this reason, these functions + * are designed to store data, such as secret keys and messages. + * + * sodium_malloc() can be used to allocate any libsodium data structure. + * + * The crypto_generichash_state structure is packed and its length is + * either 357 or 361 bytes. For this reason, when using sodium_malloc() to + * allocate a crypto_generichash_state structure, padding must be added in + * order to ensure proper alignment. crypto_generichash_statebytes() + * returns the rounded up structure size, and should be preferred to sizeof(): + * state = sodium_malloc(crypto_generichash_statebytes()); + */ + +SODIUM_EXPORT +void *sodium_malloc(const size_t size) + __attribute__ ((malloc)); + +SODIUM_EXPORT +void *sodium_allocarray(size_t count, size_t size) + __attribute__ ((malloc)); + +SODIUM_EXPORT +void sodium_free(void *ptr); + +SODIUM_EXPORT +int sodium_mprotect_noaccess(void *ptr) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int sodium_mprotect_readonly(void *ptr) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int sodium_mprotect_readwrite(void *ptr) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int sodium_pad(size_t *padded_buflen_p, unsigned char *buf, + size_t unpadded_buflen, size_t blocksize, size_t max_buflen) + __attribute__ ((nonnull(2))); + +SODIUM_EXPORT +int sodium_unpad(size_t *unpadded_buflen_p, const unsigned char *buf, + size_t padded_buflen, size_t blocksize) + __attribute__ ((nonnull(2))); + +/* -------- */ + +int _sodium_alloc_init(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/runtime/vendor/libsodium/verify.c b/packages/runtime/vendor/libsodium/verify.c new file mode 100644 index 0000000000..0c3323a50b --- /dev/null +++ b/packages/runtime/vendor/libsodium/verify.c @@ -0,0 +1,95 @@ + +#include +#include + +#include "crypto_verify_16.h" +#include "crypto_verify_32.h" +#include "crypto_verify_64.h" + +size_t +crypto_verify_16_bytes(void) +{ + return crypto_verify_16_BYTES; +} + +size_t +crypto_verify_32_bytes(void) +{ + return crypto_verify_32_BYTES; +} + +size_t +crypto_verify_64_bytes(void) +{ + return crypto_verify_64_BYTES; +} + +#if defined(HAVE_EMMINTRIN_H) && defined(__SSE2__) + +# include + +static inline int +crypto_verify_n(const unsigned char *x_, const unsigned char *y_, + const int n) +{ + const __m128i zero = _mm_setzero_si128(); + volatile __m128i v1, v2, z; + volatile int m; + int i; + + const volatile __m128i *volatile x = + (const volatile __m128i *volatile) (const void *) x_; + const volatile __m128i *volatile y = + (const volatile __m128i *volatile) (const void *) y_; + v1 = _mm_loadu_si128((const __m128i *) &x[0]); + v2 = _mm_loadu_si128((const __m128i *) &y[0]); + z = _mm_xor_si128(v1, v2); + for (i = 1; i < n / 16; i++) { + v1 = _mm_loadu_si128((const __m128i *) &x[i]); + v2 = _mm_loadu_si128((const __m128i *) &y[i]); + z = _mm_or_si128(z, _mm_xor_si128(v1, v2)); + } + m = _mm_movemask_epi8(_mm_cmpeq_epi32(z, zero)); + v1 = zero; v2 = zero; z = zero; + + return (int) (((uint32_t) m + 1U) >> 16) - 1; +} + +#else + +static inline int +crypto_verify_n(const unsigned char *x_, const unsigned char *y_, + const int n) +{ + const volatile unsigned char *volatile x = + (const volatile unsigned char *volatile) x_; + const volatile unsigned char *volatile y = + (const volatile unsigned char *volatile) y_; + volatile uint_fast16_t d = 0U; + int i; + + for (i = 0; i < n; i++) { + d |= x[i] ^ y[i]; + } + return (1 & ((d - 1) >> 8)) - 1; +} + +#endif + +int +crypto_verify_16(const unsigned char *x, const unsigned char *y) +{ + return crypto_verify_n(x, y, crypto_verify_16_BYTES); +} + +int +crypto_verify_32(const unsigned char *x, const unsigned char *y) +{ + return crypto_verify_n(x, y, crypto_verify_32_BYTES); +} + +int +crypto_verify_64(const unsigned char *x, const unsigned char *y) +{ + return crypto_verify_n(x, y, crypto_verify_64_BYTES); +} diff --git a/tests/corpus/7080-crypto-ed25519.mjs b/tests/corpus/7080-crypto-ed25519.mjs new file mode 100644 index 0000000000..355d98225e --- /dev/null +++ b/tests/corpus/7080-crypto-ed25519.mjs @@ -0,0 +1,34 @@ +// Ed25519 KeyObjects in node:crypto: the RFC 8032 test 1 key through +// PKCS#8, SubjectPublicKeyInfo and JWK export and import, its deterministic +// signatures from crypto.sign and crypto.verify, generateKeyPairSync, the +// two-argument generateKeyPair callback, and Node's errors for digests, +// Sign objects and the RSA and EC key encodings. +import * as c from "node:crypto"; +const t = (l, f) => { try { const v = f(); console.log(l, "=>", typeof v === "object" && v !== null && !Buffer.isBuffer(v) ? JSON.stringify(v) : Buffer.isBuffer(v) ? v.toString("hex") : String(v).slice(0, 200)); } catch (e) { console.log(l, "!!", e.name, e.code, e.message); } }; +const seed = Buffer.from("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", "hex"); +const der = Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), seed]); +const k = c.createPrivateKey({ key: der, format: "der", type: "pkcs8" }); +const p = c.createPublicKey(k); +t("type", () => [k.type, k.asymmetricKeyType, JSON.stringify(k.asymmetricKeyDetails), p.type].join()); +t("pkcs8 pem", () => k.export({ type: "pkcs8", format: "pem" })); +t("spki pem", () => p.export({ type: "spki", format: "pem" })); +t("spki der", () => p.export({ type: "spki", format: "der" })); +t("jwk priv", () => k.export({ format: "jwk" })); +t("jwk pub", () => p.export({ format: "jwk" })); +t("sig empty", () => c.sign(null, Buffer.alloc(0), k)); +t("sig abc", () => c.sign(undefined, Buffer.from("abc"), k)); +t("verify", () => c.verify(null, Buffer.from("abc"), p, c.sign(null, Buffer.from("abc"), k))); +t("verify bad", () => c.verify(null, Buffer.from("abd"), p, c.sign(null, Buffer.from("abc"), k))); +t("sign sha256", () => c.sign("sha256", Buffer.from("abc"), k)); +t("createSign", () => c.createSign("sha256").update("x").sign(k)); +t("pkcs1 export", () => k.export({ type: "pkcs1", format: "pem" })); +t("sec1 export", () => k.export({ type: "sec1", format: "pem" })); +t("gen cb form", () => { c.generateKeyPair("ed25519", (e, pub, priv) => console.log("cb", e, pub.asymmetricKeyType, priv.type)); return "scheduled"; }); +t("gen", () => { const g = c.generateKeyPairSync("ed25519"); return [g.publicKey.asymmetricKeyType, Object.keys(g).join()].join(); }); +t("gen opts", () => { const g = c.generateKeyPairSync("ed25519", { publicKeyEncoding: { type: "spki", format: "der" } }); return g.publicKey.length; }); +t("jwk import", () => c.createPrivateKey({ key: k.export({ format: "jwk" }), format: "jwk" }).equals(k)); +t("jwk pub import", () => c.createPublicKey({ key: p.export({ format: "jwk" }), format: "jwk" }).equals(p)); +t("dsaEncoding", () => c.sign(null, Buffer.from("x"), { key: k, dsaEncoding: "ieee-p1363" }).length); +t("verify short sig", () => c.verify(null, Buffer.from("x"), p, Buffer.alloc(10))); +t("pem parse", () => c.createPrivateKey(k.export({ type: "pkcs8", format: "pem" })).equals(k)); +t("equals ec", () => k.equals(c.generateKeyPairSync("ec", { namedCurve: "P-256" }).privateKey)); diff --git a/tests/corpus/7081-webcrypto-ed25519.mjs b/tests/corpus/7081-webcrypto-ed25519.mjs new file mode 100644 index 0000000000..5cf7728f14 --- /dev/null +++ b/tests/corpus/7081-webcrypto-ed25519.mjs @@ -0,0 +1,39 @@ +// WebCrypto Ed25519: the RFC 8032 test 1 key imported from pkcs8, jwk and +// raw, its deterministic signatures, verify, generateKey, every export +// format, and Node's DOMExceptions for usages, raw private export, short +// raw keys and mismatched JWK members. + +const t = async (label, fn) => { try { const v = await fn(); console.log(label, "=>", v instanceof ArrayBuffer ? Buffer.from(v).toString("hex") : typeof v === "object" && v !== null ? JSON.stringify(v) : v); } catch (e) { console.log(label, "!!", e.name, e.code, e.message); } }; +const seed = Buffer.from("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", "hex"); +const pkcs8 = Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), seed]); +const priv = await crypto.subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, true, ["sign"]); +await t("algorithm", () => [JSON.stringify(priv.algorithm), priv.type, priv.usages.join(), priv.extractable].join(" ")); +await t("sign empty", () => crypto.subtle.sign("Ed25519", priv, new Uint8Array(0))); +await t("sign abc", () => crypto.subtle.sign({ name: "Ed25519" }, priv, new TextEncoder().encode("abc"))); +await t("jwk priv", () => crypto.subtle.exportKey("jwk", priv)); +await t("pkcs8 back", async () => Buffer.from(await crypto.subtle.exportKey("pkcs8", priv)).equals(pkcs8)); +const jwk = await crypto.subtle.exportKey("jwk", priv); +const pubJwk = { kty: jwk.kty, crv: jwk.crv, x: jwk.x }; +const pub = await crypto.subtle.importKey("jwk", pubJwk, "Ed25519", true, ["verify"]); +await t("raw pub", () => crypto.subtle.exportKey("raw", pub)); +await t("spki pub", () => crypto.subtle.exportKey("spki", pub)); +await t("jwk pub", () => crypto.subtle.exportKey("jwk", pub)); +const sig = await crypto.subtle.sign("Ed25519", priv, new TextEncoder().encode("abc")); +await t("verify", () => crypto.subtle.verify("Ed25519", pub, sig, new TextEncoder().encode("abc"))); +await t("verify bad", () => crypto.subtle.verify("Ed25519", pub, sig, new TextEncoder().encode("abd"))); +await t("verify short", () => crypto.subtle.verify("Ed25519", pub, new Uint8Array(10), new TextEncoder().encode("abc"))); +const rawImported = await crypto.subtle.importKey("raw", await crypto.subtle.exportKey("raw", pub), { name: "Ed25519" }, false, ["verify"]); +await t("raw import verify", () => crypto.subtle.verify("Ed25519", rawImported, sig, new TextEncoder().encode("abc"))); +const pair = await crypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]); +await t("generated", async () => [pair.privateKey.usages.join(), pair.publicKey.usages.join(), pair.publicKey.extractable, (await crypto.subtle.exportKey("raw", pair.publicKey)).byteLength, await crypto.subtle.verify("Ed25519", pair.publicKey, await crypto.subtle.sign("Ed25519", pair.privateKey, new Uint8Array([1])), new Uint8Array([1]))].join(" ")); +await t("raw private", () => crypto.subtle.exportKey("raw", pair.privateKey)); +await t("bad usage", () => crypto.subtle.generateKey({ name: "Ed25519" }, true, ["encrypt"])); +await t("verify only", () => crypto.subtle.generateKey("Ed25519", true, ["verify"])); +await t("raw short", () => crypto.subtle.importKey("raw", new Uint8Array(31), { name: "Ed25519" }, true, ["verify"])); +await t("raw sign usage", () => crypto.subtle.importKey("raw", new Uint8Array(32), "Ed25519", true, ["sign"])); +await t("jwk alg EdDSA", async () => (await crypto.subtle.importKey("jwk", { ...pubJwk, alg: "EdDSA" }, "Ed25519", true, ["verify"])).type); +await t("jwk alg bad", () => crypto.subtle.importKey("jwk", { ...pubJwk, alg: "RS256" }, "Ed25519", true, ["verify"])); +await t("jwk crv bad", () => crypto.subtle.importKey("jwk", { ...pubJwk, crv: "Ed448" }, "Ed25519", true, ["verify"])); +await t("jwk kty bad", () => crypto.subtle.importKey("jwk", { ...pubJwk, kty: "EC" }, "Ed25519", true, ["verify"])); +await t("ecdsa key as ed", async () => { const e = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]); return crypto.subtle.importKey("spki", await crypto.subtle.exportKey("spki", e.publicKey), "Ed25519", true, ["verify"]); }); +await t("sign wrong alg", () => crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, priv, new Uint8Array(1))); diff --git a/tests/corpus/7082-crypto-key-objects.ts b/tests/corpus/7082-crypto-key-objects.ts new file mode 100644 index 0000000000..816dd6be08 --- /dev/null +++ b/tests/corpus/7082-crypto-key-objects.ts @@ -0,0 +1,132 @@ +// Private and public KeyObjects: createPrivateKey and createPublicKey from +// PEM, DER, JWK, a private KeyObject and a certificate; the type, +// asymmetricKeyType and asymmetricKeyDetails getters; export() as PKCS#1, +// SEC1, PKCS#8, SubjectPublicKeyInfo, DER and JWK; equals(); instanceof; +// and Node's errors for invalid options and inputs, including a private or +// public key passed to createHmac. +import * as crypto from "node:crypto"; + +const RSA = `-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQDWei4BREJm+4C6u/+F4NI7kTOQ9gf/a+Ecd2SZxfpXRtXT0QVN +MAI1uQNmB4abYzvd1G2V2XKsTJFJcJWibjAz56UxtxxggTpSAalFZYoPm6HfabwJ +b/YWh0K0OrNFvdkeXLLMiHQsxmIQ2lrFoycQEpZ4/yxjLe6LQF+L79k2ywIDAQAB +AoGAMWS/9ZC3zim7ssWbCkUFhaxh+M+AdgVtOXYRmAxtI+5vTjHEC8uzppWuP7Aa +Au0um17RwgHrE90qVIR2IYtI1C9K9RLUNlKKVPqnSSmr0qkXR246R/Zd8eho1XSS +MX6KmnbmRijUx9Rw1AbD2ZmYCrXsPf2HsCdH+SGQ1ZTunnECQQD090dTA21Jyu/b +ADglqgixJCPIoc6qpxst+W1LreIo4UvkSmgnRYvsKDlM7FNI+6f7igI625KXIAnl +xVlmGLXDAkEA4CNVeP07fmQUBs1aPQ9LCOvN42SV6S8Fjbf4XHIYG5Kp48/98oJj +1pFsXNa8nn8fmN9zFw8yM/yd22iZdueCWQJAWMja//po/OrhniVpNDV5awKnOYtz +xSRF1JbnhxthseFeSUKylzE8sn4trnSlBvm2+5MclcqJSd7h13Z8nWVtUwJAZ0SA +Ve0+ov4TKAsnH2jTZnnWc4VdeIFcqq3vdRBwQQvy0G59D2W6ABsidSfO4auqcDHL +gf1TYRZr2LI5usnxiQJAGt3e0QC0q0WN4p49YhyXXVuHccx79RFgfN7V1lkAyYuL +2bPV41mEtD/AbSrObtqqvOaWZjwSr8JCyPdPZZAIMw== +-----END RSA PRIVATE KEY----- +`; +const EC = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEILpVcXOEZQXpH/q3ec3AsLg4x4Bld6uZLqDC56OYKQK4oAoGCCqGSM49 +AwEHoUQDQgAE0gwi0a/4kuVxjsO3vw79rT3QSxnPY7sooVlI+ENq35D7meWZv/dp +FTGFte7Bd8lFxaZ10dMo0tl++AOtt7umPg== +-----END EC PRIVATE KEY----- +`; +const EC384 = `-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDDer7W8pQD96zdS8UWxg34nMTxqREszwP6niYYOdw75LHPmHFKWeMKW +dmG6d3n4bRugBwYFK4EEACKhZANiAATIQKDRVJ501CTQtbK2IslxdO8Oma2ZU4ee +GRuQFg6ajhKXa0iCU5o9rpdObfCecyiYApdtVpahD2SwPua5CZyq/0jUn9frwKtB +uEeB/3T/yYRCD4zGUc7TtUDh8c7Gt5c= +-----END EC PRIVATE KEY----- +`; +const CERT = `-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIPAPAN+s4AESIzRFVmd4iZMA0GCSqGSIb3DQEBDAUAMIGc +MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2Fu +IEZyYW5jaXNjbzEUMBIGA1UECgwLRXhhbXBsZSBPcmcxETAPBgNVBAsMCERldiBU +ZWFtMRgwFgYDVQQDDA9FeGFtcGxlIFJvb3QgQ0ExHTAbBgkqhkiG9w0BCQEWDmNh +QGV4YW1wbGUuY29tMB4XDTI2MTAyMTE2MzM1MloXDTM2MTAxODE2MzM1MlowJzER +MA8GA1UECgwITGVhZiBPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABJVfVy9EH/VHQuRqpFFnXk0F00ncDht6APgVPDoybK8H +PlcM4T5H1yAOMHnlH29LK1jtMqU1aAOiiQRzPdCvqoujggE3MIIBMzAJBgNVHRME +AjAAMGgGA1UdEQRhMF+CCWxvY2FsaG9zdIINKi5leGFtcGxlLmNvbYcEfwAAAYcQ +AAAAAAAAAAAAAAAAAAAAAYERYWRtaW5AZXhhbXBsZS5jb22GGGh0dHBzOi8vZXhh +bXBsZS5jb20vcGF0aDBdBggrBgEFBQcBAQRRME8wIwYIKwYBBQUHMAGGF2h0dHA6 +Ly9vY3NwLmV4YW1wbGUuY29tMCgGCCsGAQUFBzAChhxodHRwOi8vY2EuZXhhbXBs +ZS5jb20vY2EuY3J0MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNV +HQ4EFgQUdKIx6ULFLPNP/kzwUFjSa5D19RswHwYDVR0jBBgwFoAUP+yQyItpNYEl +s26Jfv+NnciGaK8wDQYJKoZIhvcNAQEMBQADggEBABmQ8f2VsXNaMM9X1MEhi27D +LjwcOaaGEINNZq7xu/HL6UljMb1Odsuz66DkiH3Exd3MrWy6OlOpJJF0VzsqMsoD +0CRBxXF28o6Jil3vOGVwlYNmRidpkAkj4VyWDvv8FCS9VbsBtXaSHwu9wcCwePE0 +cGJRfTKoZ6KJ8vQ57DBEZ/ZszuhehOZYqrTmoUdjZ123kXdxQvK+ALn4SlIXHBhN +9unhd+8i00EmkXO/REw8paMcMVVGR+ioOEjyNHqZo6vkRqgpKggn1Yq8cTTDOMnS +P1o42EQZLlbuvx0kqInBqwqjDx4nZiPaifoDc9BF6iZPBvm8dhrEfghfB1tkNk0= +-----END CERTIFICATE----- +`; + +function attempt(label: string, run: () => unknown): void { + try { + const value = run(); + console.log(label, typeof value === "string" ? JSON.stringify(value) : value); + } catch (e) { + const err = e as NodeJS.ErrnoException; + console.log(label, "throws", err.name, err.code, err.message); + } +} + +const rsa = crypto.createPrivateKey(RSA); +const ec = crypto.createPrivateKey(Buffer.from(EC)); +const ec384 = crypto.createPrivateKey({ key: EC384, format: "pem" }); +const rsaPublic = crypto.createPublicKey(rsa); +const ecPublic = crypto.createPublicKey(EC); +const certPublic = crypto.createPublicKey(CERT); +const keys: [string, crypto.KeyObject][] = [ + ["rsa", rsa], + ["ec", ec], + ["ec384", ec384], + ["rsaPublic", rsaPublic], + ["ecPublic", ecPublic], + ["certPublic", certPublic], +]; +for (const [label, key] of keys) { + console.log(label, key.type, key.asymmetricKeyType, key.asymmetricKeyDetails, key instanceof crypto.KeyObject); +} + +attempt("pkcs1", () => rsa.export({ type: "pkcs1", format: "pem" }) === RSA + "\n"); +attempt("sec1", () => ec.export({ type: "sec1", format: "pem" }) === EC + "\n"); +attempt("rsa pkcs8", () => rsa.export({ type: "pkcs8", format: "pem" })); +attempt("ec pkcs8", () => ec.export({ type: "pkcs8", format: "pem" })); +attempt("rsa spki", () => rsaPublic.export({ type: "spki", format: "pem" })); +attempt("rsa public pkcs1", () => rsaPublic.export({ type: "pkcs1", format: "pem" })); +attempt("ec spki", () => ecPublic.export({ type: "spki", format: "pem" })); +attempt("cert spki", () => certPublic.export({ type: "spki", format: "pem" })); +attempt("der", () => (rsa.export({ type: "pkcs8", format: "der" }) as Buffer).toString("base64")); +attempt("ec jwk", () => JSON.stringify(ec.export({ format: "jwk" }))); +attempt("ec384 public jwk", () => JSON.stringify(crypto.createPublicKey(ec384).export({ format: "jwk" }))); +attempt("rsa jwk", () => JSON.stringify(rsa.export({ format: "jwk" }))); +attempt("rsa public jwk", () => JSON.stringify(rsaPublic.export({ format: "jwk" }))); + +// Round trips through DER and JWK. +const der = rsa.export({ type: "pkcs1", format: "der" }) as Buffer; +attempt("from der", () => crypto.createPrivateKey({ key: der, format: "der", type: "pkcs1" }).equals(rsa)); +const spki = ecPublic.export({ type: "spki", format: "der" }) as Buffer; +attempt("public from der", () => crypto.createPublicKey({ key: spki, format: "der", type: "spki" }).equals(ecPublic)); +attempt("from jwk", () => crypto.createPrivateKey({ key: ec.export({ format: "jwk" }), format: "jwk" }).equals(ec)); +attempt("rsa from jwk", () => crypto.createPrivateKey({ key: rsa.export({ format: "jwk" }), format: "jwk" }).equals(rsa)); +attempt("public from jwk", () => crypto.createPublicKey({ key: rsaPublic.export({ format: "jwk" }), format: "jwk" }).equals(rsaPublic)); +attempt("pkcs8 parse", () => crypto.createPrivateKey(ec.export({ type: "pkcs8", format: "pem" })).equals(ec)); +attempt("equals", () => [rsaPublic.equals(crypto.createPublicKey(RSA)), ec.equals(ecPublic), ec.equals(ec384)].join()); +attempt("tag", () => Object.prototype.toString.call(rsa)); + +// Errors. +attempt("export no options", () => rsa.export()); +attempt("export no format", () => rsa.export({} as crypto.AsymmetricKeyExportOptions<"pem">)); +attempt("export no type", () => rsa.export({ format: "pem" } as crypto.AsymmetricKeyExportOptions<"pem">)); +attempt("export spki private", () => rsa.export({ type: "spki", format: "pem" })); +attempt("export sec1 rsa", () => rsa.export({ type: "sec1", format: "pem" })); +attempt("export pkcs1 ec", () => ec.export({ type: "pkcs1", format: "der" })); +attempt("export pkcs8 public", () => ecPublic.export({ type: "pkcs8", format: "pem" })); +attempt("private from public pem", () => crypto.createPrivateKey(ecPublic.export({ type: "spki", format: "pem" }) as string)); +attempt("private from garbage", () => crypto.createPrivateKey("not a key")); +attempt("public from public", () => crypto.createPublicKey(ecPublic)); +attempt("der without type", () => crypto.createPrivateKey({ key: der, format: "der" } as crypto.PrivateKeyInput)); +attempt("hmac private key", () => crypto.createHmac("sha256", rsa)); +attempt("hmac public key", () => crypto.createHmac("sha256", ecPublic)); +// Last: OpenSSL keeps an error queued after comparing keys of different +// types, which a later call in Node can report. +attempt("equals other type", () => rsa.equals(ec)); diff --git a/tests/corpus/7083-crypto-sign-verify.ts b/tests/corpus/7083-crypto-sign-verify.ts new file mode 100644 index 0000000000..437dc16404 --- /dev/null +++ b/tests/corpus/7083-crypto-sign-verify.ts @@ -0,0 +1,85 @@ +// crypto.sign and crypto.verify, and the Sign and Verify objects: RSA +// PKCS#1 v1.5 signatures (deterministic, so they compare with Node), RSA-PSS, +// ECDSA in DER and IEEE P1363 encodings, keys given as KeyObjects, PEM and +// option objects, output and signature encodings, and Node's errors. +import * as crypto from "node:crypto"; + +const RSA = `-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQDWei4BREJm+4C6u/+F4NI7kTOQ9gf/a+Ecd2SZxfpXRtXT0QVN +MAI1uQNmB4abYzvd1G2V2XKsTJFJcJWibjAz56UxtxxggTpSAalFZYoPm6HfabwJ +b/YWh0K0OrNFvdkeXLLMiHQsxmIQ2lrFoycQEpZ4/yxjLe6LQF+L79k2ywIDAQAB +AoGAMWS/9ZC3zim7ssWbCkUFhaxh+M+AdgVtOXYRmAxtI+5vTjHEC8uzppWuP7Aa +Au0um17RwgHrE90qVIR2IYtI1C9K9RLUNlKKVPqnSSmr0qkXR246R/Zd8eho1XSS +MX6KmnbmRijUx9Rw1AbD2ZmYCrXsPf2HsCdH+SGQ1ZTunnECQQD090dTA21Jyu/b +ADglqgixJCPIoc6qpxst+W1LreIo4UvkSmgnRYvsKDlM7FNI+6f7igI625KXIAnl +xVlmGLXDAkEA4CNVeP07fmQUBs1aPQ9LCOvN42SV6S8Fjbf4XHIYG5Kp48/98oJj +1pFsXNa8nn8fmN9zFw8yM/yd22iZdueCWQJAWMja//po/OrhniVpNDV5awKnOYtz +xSRF1JbnhxthseFeSUKylzE8sn4trnSlBvm2+5MclcqJSd7h13Z8nWVtUwJAZ0SA +Ve0+ov4TKAsnH2jTZnnWc4VdeIFcqq3vdRBwQQvy0G59D2W6ABsidSfO4auqcDHL +gf1TYRZr2LI5usnxiQJAGt3e0QC0q0WN4p49YhyXXVuHccx79RFgfN7V1lkAyYuL +2bPV41mEtD/AbSrObtqqvOaWZjwSr8JCyPdPZZAIMw== +-----END RSA PRIVATE KEY----- +`; +const EC = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEILpVcXOEZQXpH/q3ec3AsLg4x4Bld6uZLqDC56OYKQK4oAoGCCqGSM49 +AwEHoUQDQgAE0gwi0a/4kuVxjsO3vw79rT3QSxnPY7sooVlI+ENq35D7meWZv/dp +FTGFte7Bd8lFxaZ10dMo0tl++AOtt7umPg== +-----END EC PRIVATE KEY----- +`; + +function attempt(label: string, run: () => unknown): void { + try { + console.log(label, run()); + } catch (e) { + const err = e as NodeJS.ErrnoException; + console.log(label, "throws", err.name, err.code, err.message); + } +} + +const rsa = crypto.createPrivateKey(RSA); +const rsaPublic = crypto.createPublicKey(rsa); +const ec = crypto.createPrivateKey(EC); +const ecPublic = crypto.createPublicKey(ec); +const data = Buffer.from("signed message"); + +for (const digest of ["sha1", "sha224", "sha256", "SHA384", "RSA-SHA512", "sha256WithRSAEncryption"]) { + const signature = crypto.sign(digest, data, rsa); + console.log(digest, signature.toString("base64"), crypto.verify(digest, data, rsaPublic, signature)); +} +attempt("null digest", () => crypto.sign(null, data, RSA).equals(crypto.sign("sha256", data, rsa))); +attempt("private key verifies", () => crypto.verify("sha256", data, RSA, crypto.sign("sha256", data, rsa))); +attempt("wrong data", () => crypto.verify("sha256", Buffer.from("other"), rsaPublic, crypto.sign("sha256", data, rsa))); + +const pss = crypto.sign("sha256", data, { key: rsa, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 32 }); +attempt("pss", () => [pss.length, crypto.verify("sha256", data, { key: rsaPublic, padding: crypto.constants.RSA_PKCS1_PSS_PADDING }, pss)].join()); +attempt("pss as pkcs1", () => crypto.verify("sha256", data, rsaPublic, pss)); +attempt("pss digest salt", () => + crypto.verify("sha256", data, { key: rsaPublic, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST }, pss), +); + +const der = crypto.sign("sha256", data, ec); +attempt("ecdsa der", () => [der[0], crypto.verify("sha256", data, ecPublic, der)].join()); +const p1363 = crypto.sign("sha384", data, { key: EC, dsaEncoding: "ieee-p1363" }); +attempt("ecdsa p1363", () => [p1363.length, crypto.verify("sha384", data, { key: ecPublic, dsaEncoding: "ieee-p1363" }, p1363)].join()); +attempt("p1363 as der", () => crypto.verify("sha384", data, ecPublic, p1363)); +attempt("truncated", () => crypto.verify("sha256", data, ecPublic, der.subarray(0, 10))); + +const signer = crypto.createSign("SHA256"); +signer.update("signed "); +signer.update(Buffer.from("message")); +const hex = signer.sign(rsa, "hex"); +attempt("sign object", () => hex === crypto.sign("sha256", data, rsa).toString("hex")); +attempt("sign again", () => signer.sign(rsa)); +attempt("update after sign", () => signer.update("more")); +const verifier = crypto.createVerify("RSA-SHA256"); +verifier.update("signed message", "utf8"); +attempt("verify object", () => verifier.verify(RSA, Buffer.from(hex, "hex").toString("base64"), "base64")); +attempt("verify again", () => verifier.verify(rsaPublic, hex, "hex")); +const ecSigner = crypto.createSign("sha512").update("chained"); +const ecSignature = ecSigner.sign({ key: ec, dsaEncoding: "ieee-p1363" }); +attempt("chained", () => crypto.createVerify("sha512").update("chained").verify({ key: ecPublic, dsaEncoding: "ieee-p1363" }, ecSignature)); + +attempt("invalid digest", () => crypto.sign("nope", data, rsa)); +attempt("public key signs", () => crypto.sign("sha256", data, rsaPublic)); +attempt("invalid createSign", () => crypto.createSign("nope")); +attempt("invalid dsaEncoding", () => crypto.sign("sha256", data, { key: ec, dsaEncoding: "x" as "der" })); diff --git a/tests/corpus/7084-crypto-key-pairs.cjs b/tests/corpus/7084-crypto-key-pairs.cjs new file mode 100644 index 0000000000..ed4db03106 --- /dev/null +++ b/tests/corpus/7084-crypto-key-pairs.cjs @@ -0,0 +1,64 @@ +// generateKeyPairSync and generateKeyPair for RSA and EC keys: KeyObject +// results, PEM and DER encodings, curve names and NIST aliases, the +// callback's later macrotask, and Node's errors for invalid types and +// options. +const crypto = require("node:crypto"); + +function attempt(label, run) { + try { + console.log(label, run()); + } catch (e) { + console.log(label, "throws", e.name, e.code, e.message); + } +} + +const data = Buffer.from("key pair"); +for (const curve of ["prime256v1", "P-256", "secp384r1", "P-384", "secp521r1", "P-521", "secp256k1"]) { + const { publicKey, privateKey } = crypto.generateKeyPairSync("ec", { namedCurve: curve }); + const signature = crypto.sign("sha256", data, privateKey); + console.log(curve, privateKey.type, publicKey.type, privateKey.asymmetricKeyDetails, crypto.verify("sha256", data, publicKey, signature)); +} +const rsa = crypto.generateKeyPairSync("rsa", { modulusLength: 1024 }); +console.log("rsa", rsa.privateKey.asymmetricKeyDetails, Object.keys(rsa)); +const exponent = crypto.generateKeyPairSync("rsa", { modulusLength: 1024, publicExponent: 3 }); +console.log("exponent", exponent.publicKey.asymmetricKeyDetails); +const encoded = crypto.generateKeyPairSync("ec", { + namedCurve: "P-256", + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "sec1", format: "der" }, +}); +console.log("encoded", typeof encoded.publicKey, encoded.publicKey.split("\n")[0], Buffer.isBuffer(encoded.privateKey)); +const reparsed = crypto.createPrivateKey({ key: encoded.privateKey, format: "der", type: "sec1" }); +console.log("reparsed", crypto.createPublicKey(reparsed).export({ type: "spki", format: "pem" }) === encoded.publicKey); +const rsaPem = crypto.generateKeyPairSync("rsa", { + modulusLength: 1024, + publicKeyEncoding: { type: "pkcs1", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "pem" }, +}); +console.log("rsa pem", rsaPem.publicKey.split("\n")[0], rsaPem.privateKey.split("\n")[0]); +const jwk = crypto.generateKeyPairSync("ec", { namedCurve: "secp256k1", publicKeyEncoding: { format: "jwk" } }); +console.log("jwk", Object.keys(jwk.publicKey), jwk.publicKey.crv); + +attempt("no options", () => crypto.generateKeyPairSync("rsa")); +attempt("no modulusLength", () => crypto.generateKeyPairSync("rsa", {})); +attempt("string modulusLength", () => crypto.generateKeyPairSync("rsa", { modulusLength: "2048" })); +attempt("negative modulusLength", () => crypto.generateKeyPairSync("rsa", { modulusLength: -1 })); +attempt("unknown type", () => crypto.generateKeyPairSync("foo", {})); +attempt("number type", () => crypto.generateKeyPairSync(5, {})); +attempt("no curve", () => crypto.generateKeyPairSync("ec", {})); +attempt("unknown curve", () => crypto.generateKeyPairSync("ec", { namedCurve: "nope" })); +attempt("bad encoding format", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: { type: "spki", format: "xml" } })); +attempt("bad encoding type", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", privateKeyEncoding: { type: "spki", format: "pem" } })); +attempt("encoding not object", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: "pem" })); + +let returned = false; +crypto.generateKeyPair("ec", { namedCurve: "P-384" }, (err, publicKey, privateKey) => { + console.log("callback", returned, err, publicKey.type, privateKey.asymmetricKeyDetails); + crypto.generateKeyPair("rsa", { modulusLength: 1024 }, (err2, publicKey2) => { + console.log("rsa callback", err2, publicKey2.asymmetricKeyDetails.modulusLength); + }); +}); +process.nextTick(() => console.log("tick")); +attempt("callback errors throw", () => crypto.generateKeyPair("ec", {}, () => console.log("never"))); +returned = true; +console.log("returned"); diff --git a/tests/corpus/7085-webcrypto-ecdsa-rsa.ts b/tests/corpus/7085-webcrypto-ecdsa-rsa.ts new file mode 100644 index 0000000000..446ed7c635 --- /dev/null +++ b/tests/corpus/7085-webcrypto-ecdsa-rsa.ts @@ -0,0 +1,66 @@ +// WebCrypto ECDSA, RSASSA-PKCS1-v1_5 and RSA-PSS: importKey from spki, +// pkcs8, jwk and raw, exportKey to each format, sign and verify (RSA +// PKCS#1 v1.5 signatures are deterministic, so they compare with Node), +// generateKey for every curve and both RSA algorithms, and the CryptoKey +// members of these keys. +const RSA_PKCS8 = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBANZ6LgFEQmb7gLq7/4Xg0juRM5D2B/9r4Rx3ZJnF+ldG1dPRBU0wAjW5A2YHhptjO93UbZXZcqxMkUlwlaJuMDPnpTG3HGCBOlIBqUVlig+bod9pvAlv9haHQrQ6s0W92R5cssyIdCzGYhDaWsWjJxASlnj/LGMt7otAX4vv2TbLAgMBAAECgYAxZL/1kLfOKbuyxZsKRQWFrGH4z4B2BW05dhGYDG0j7m9OMcQLy7Omla4/sBoC7S6bXtHCAesT3SpUhHYhi0jUL0r1EtQ2UopU+qdJKavSqRdHbjpH9l3x6GjVdJIxfoqaduZGKNTH1HDUBsPZmZgKtew9/YewJ0f5IZDVlO6ecQJBAPT3R1MDbUnK79sAOCWqCLEkI8ihzqqnGy35bUut4ijhS+RKaCdFi+woOUzsU0j7p/uKAjrbkpcgCeXFWWYYtcMCQQDgI1V4/Tt+ZBQGzVo9D0sI683jZJXpLwWNt/hcchgbkqnjz/3ygmPWkWxc1ryefx+Y33MXDzIz/J3baJl254JZAkBYyNr/+mj86uGeJWk0NXlrAqc5i3PFJEXUlueHG2Gx4V5JQrKXMTyyfi2udKUG+bb7kxyVyolJ3uHXdnydZW1TAkBnRIBV7T6i/hMoCycfaNNmedZzhV14gVyqre91EHBBC/LQbn0PZboAGyJ1J87hq6pwMcuB/VNhFmvYsjm6yfGJAkAa3d7RALSrRY3inj1iHJddW4dxzHv1EWB83tXWWQDJi4vZs9XjWYS0P8BtKs5u2qq85pZmPBKvwkLI909lkAgz"; +const RSA_SPKI = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDWei4BREJm+4C6u/+F4NI7kTOQ9gf/a+Ecd2SZxfpXRtXT0QVNMAI1uQNmB4abYzvd1G2V2XKsTJFJcJWibjAz56UxtxxggTpSAalFZYoPm6HfabwJb/YWh0K0OrNFvdkeXLLMiHQsxmIQ2lrFoycQEpZ4/yxjLe6LQF+L79k2ywIDAQAB"; +const EC_PKCS8 = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgulVxc4RlBekf+rd5zcCwuDjHgGV3q5kuoMLno5gpArihRANCAATSDCLRr/iS5XGOw7e/Dv2tPdBLGc9juyihWUj4Q2rfkPuZ5Zm/92kVMYW17sF3yUXFpnXR0yjS2X74A623u6Y+"; +const EC_SPKI = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0gwi0a/4kuVxjsO3vw79rT3QSxnPY7sooVlI+ENq35D7meWZv/dpFTGFte7Bd8lFxaZ10dMo0tl++AOtt7umPg=="; + +const bytes = (text: string): Uint8Array => new Uint8Array(Buffer.from(text, "base64")); +const hex = (buffer: ArrayBuffer): string => Buffer.from(buffer).toString("hex"); +const data = new TextEncoder().encode("webcrypto message"); + +const rsaPrivate = await crypto.subtle.importKey("pkcs8", bytes(RSA_PKCS8), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, ["sign"]); +const rsaPublic = await crypto.subtle.importKey("spki", bytes(RSA_SPKI), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, ["verify"]); +console.log(rsaPrivate.type, rsaPublic.type, rsaPrivate.extractable, rsaPublic.usages); +const rsaAlgorithm = rsaPrivate.algorithm as RsaHashedKeyAlgorithm; +console.log(rsaAlgorithm.name, rsaAlgorithm.modulusLength, Array.from(rsaAlgorithm.publicExponent), rsaAlgorithm.hash.name); +const rsaSignature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", rsaPrivate, data); +console.log("rsa signature", hex(rsaSignature)); +console.log("rsa verify", await crypto.subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, rsaPublic, rsaSignature, data)); +console.log("rsa spki", Buffer.from(await crypto.subtle.exportKey("spki", rsaPublic)).toString("base64") === RSA_SPKI); +console.log("rsa pkcs8", Buffer.from(await crypto.subtle.exportKey("pkcs8", rsaPrivate)).toString("base64") === RSA_PKCS8); +const rsaJwk = await crypto.subtle.exportKey("jwk", rsaPublic); +console.log("rsa jwk", JSON.stringify(rsaJwk)); +const fromJwk = await crypto.subtle.importKey("jwk", rsaJwk, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["verify"]); +console.log("rsa jwk verify", await crypto.subtle.verify("RSASSA-PKCS1-v1_5", fromJwk, rsaSignature, data), fromJwk.extractable); + +const ecPrivate = await crypto.subtle.importKey("pkcs8", bytes(EC_PKCS8), { name: "ECDSA", namedCurve: "P-256" }, true, ["sign"]); +const ecPublic = await crypto.subtle.importKey("spki", bytes(EC_SPKI), { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]); +console.log(JSON.stringify(ecPrivate.algorithm), ecPrivate.usages, ecPublic.usages); +console.log("ec jwk", JSON.stringify(await crypto.subtle.exportKey("jwk", ecPrivate))); +const point = await crypto.subtle.exportKey("raw", ecPublic); +console.log("ec raw", hex(point)); +const fromPoint = await crypto.subtle.importKey("raw", point, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]); +for (const hash of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) { + const signature = await crypto.subtle.sign({ name: "ECDSA", hash }, ecPrivate, data); + console.log("ecdsa", hash, signature.byteLength, await crypto.subtle.verify({ name: "ECDSA", hash: { name: hash } }, fromPoint, signature, data)); +} +const ecSignature = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, ecPrivate, data); +const tampered = new Uint8Array(ecSignature); +tampered[5] ^= 1; +console.log("tampered", await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, ecPublic, tampered, data)); +console.log("other data", await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, ecPublic, ecSignature, new Uint8Array([1]))); + +for (const namedCurve of ["P-256", "P-384", "P-521"]) { + const pair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve }, false, ["sign", "verify"]); + const signature = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-384" }, pair.privateKey, data); + console.log(namedCurve, Object.keys(pair).join(), pair.privateKey.extractable, pair.publicKey.extractable, signature.byteLength, + await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-384" }, pair.publicKey, signature, data), + (await crypto.subtle.exportKey("raw", pair.publicKey)).byteLength); +} +for (const name of ["RSASSA-PKCS1-v1_5", "RSA-PSS"]) { + const pair = await crypto.subtle.generateKey({ name, modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-512" }, true, ["sign", "verify"]); + const params = { name, saltLength: 20 }; + const signature = await crypto.subtle.sign(params, pair.privateKey, data); + const jwk = await crypto.subtle.exportKey("jwk", pair.privateKey); + console.log(name, signature.byteLength, await crypto.subtle.verify(params, pair.publicKey, signature, data), jwk.alg, Object.keys(jwk).join()); +} +const pss = await crypto.subtle.importKey("pkcs8", bytes(RSA_PKCS8), { name: "RSA-PSS", hash: "SHA-256" }, false, ["sign"]); +const pssPublic = await crypto.subtle.importKey("spki", bytes(RSA_SPKI), { name: "RSA-PSS", hash: "SHA-256" }, false, ["verify"]); +const pssSignature = await crypto.subtle.sign({ name: "RSA-PSS", saltLength: 32 }, pss, data); +console.log("pss", await crypto.subtle.verify({ name: "RSA-PSS", saltLength: 32 }, pssPublic, pssSignature, data), + await crypto.subtle.verify({ name: "RSA-PSS", saltLength: 0 }, pssPublic, pssSignature, data)); +console.log(ecPrivate instanceof CryptoKey, String(ecPublic), Object.prototype.toString.call(rsaPublic)); diff --git a/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs b/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs new file mode 100644 index 0000000000..62292fbddb --- /dev/null +++ b/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs @@ -0,0 +1,58 @@ +// WebCrypto ECDSA and RSA errors: Node's TypeErrors for missing algorithm +// members, and its DOMExceptions for curves, usages, key types, formats, +// extractability and JWK members, from generateKey, importKey, exportKey, +// sign and verify. + +const t = async (label, fn) => { try { const v = await fn(); console.log(label, "=>", v instanceof ArrayBuffer ? "AB" + v.byteLength : typeof v === "object" && v !== null ? JSON.stringify(v) : v); } catch (e) { console.log(label, "!!", e.name, e.code, e.message); } }; +const e3 = new Uint8Array([1, 0, 1]); +await t("rsa no hash", () => crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, publicExponent: e3 }, true, ["sign"])); +await t("rsa no len", () => crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", publicExponent: e3, hash: "SHA-256" }, true, ["sign"])); +await t("rsa no exp", () => crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, hash: "SHA-256" }, true, ["sign"])); +await t("rsa bad usage", () => crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, publicExponent: e3, hash: "SHA-256" }, true, ["encrypt"])); +await t("pss bad usage", () => crypto.subtle.generateKey({ name: "RSA-PSS", modulusLength: 1024, publicExponent: e3, hash: "SHA-256" }, true, ["encrypt"])); +await t("rsa bad hash", () => crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, publicExponent: e3, hash: "MD5" }, true, ["sign"])); +await t("rsa exp 3", async () => (await crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, publicExponent: new Uint8Array([3]), hash: "SHA-256" }, true, ["sign"])).publicKey.algorithm.publicExponent.length); +await t("rsa exp 4", () => crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, publicExponent: new Uint8Array([4]), hash: "SHA-256" }, true, ["sign"])); +await t("rsa small", () => crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 256, publicExponent: e3, hash: "SHA-256" }, true, ["sign"])); +await t("ecdsa no curve", () => crypto.subtle.generateKey({ name: "ECDSA" }, true, ["sign"])); +await t("unknown alg", () => crypto.subtle.generateKey({ name: "FOO" }, true, ["sign"])); +const ec = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]); +await t("export bad format", () => crypto.subtle.exportKey("foo", ec.publicKey)); +await t("export not key", () => crypto.subtle.exportKey("spki", {})); +await t("export spki private", () => crypto.subtle.exportKey("spki", ec.privateKey)); +await t("export pkcs8 public", () => crypto.subtle.exportKey("pkcs8", ec.publicKey)); +await t("import raw private", () => crypto.subtle.importKey("raw", new Uint8Array(32), { name: "ECDSA", namedCurve: "P-256" }, true, ["sign"])); +await t("import pkcs8 verify", async () => crypto.subtle.importKey("pkcs8", await crypto.subtle.exportKey("pkcs8", ec.privateKey), { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"])); +await t("import pkcs8 empty", async () => crypto.subtle.importKey("pkcs8", await crypto.subtle.exportKey("pkcs8", ec.privateKey), { name: "ECDSA", namedCurve: "P-256" }, true, [])); +await t("import spki empty", async () => (await crypto.subtle.importKey("spki", await crypto.subtle.exportKey("spki", ec.publicKey), { name: "ECDSA", namedCurve: "P-256" }, true, [])).usages); +await t("import no curve", async () => crypto.subtle.importKey("spki", await crypto.subtle.exportKey("spki", ec.publicKey), { name: "ECDSA" }, true, ["verify"])); +await t("import rsa no hash", async () => crypto.subtle.importKey("spki", await crypto.subtle.exportKey("spki", ec.publicKey), { name: "RSASSA-PKCS1-v1_5" }, true, ["verify"])); +await t("import ec as rsa", async () => crypto.subtle.importKey("spki", await crypto.subtle.exportKey("spki", ec.publicKey), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, ["verify"])); +await t("import jwk bad", () => crypto.subtle.importKey("jwk", { kty: "EC", crv: "P-256", x: "AA", y: "AA" }, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"])); +await t("import jwk crv mismatch", async () => crypto.subtle.importKey("jwk", await crypto.subtle.exportKey("jwk", ec.publicKey), { name: "ECDSA", namedCurve: "P-384" }, true, ["verify"])); +await t("import jwk kty", async () => crypto.subtle.importKey("jwk", { ...(await crypto.subtle.exportKey("jwk", ec.publicKey)), kty: "RSA" }, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"])); +await t("jwk ext false", async () => crypto.subtle.importKey("jwk", { ...(await crypto.subtle.exportKey("jwk", ec.publicKey)), ext: false }, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"])); +await t("jwk key_ops", async () => crypto.subtle.importKey("jwk", { ...(await crypto.subtle.exportKey("jwk", ec.publicKey)), key_ops: ["sign"] }, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"])); +const data = new Uint8Array([1, 2, 3]); +const spki = await crypto.subtle.exportKey("spki", ec.publicKey); +const rsa = await crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, false, ["sign", "verify"]); +const pss = await crypto.subtle.generateKey({ name: "RSA-PSS", modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-384" }, true, ["sign", "verify"]); +await t("export nonextractable", () => crypto.subtle.exportKey("pkcs8", rsa.privateKey)); +await t("export raw private", () => crypto.subtle.exportKey("raw", ec.privateKey)); +await t("sign with public", () => crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, ec.publicKey, data)); +await t("sign wrong alg", () => crypto.subtle.sign("RSASSA-PKCS1-v1_5", ec.privateKey, data)); +await t("ecdsa no hash", () => crypto.subtle.sign({ name: "ECDSA" }, ec.privateKey, data)); +await t("bad curve", () => crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-999" }, true, ["sign"])); +await t("bad usage", () => crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["encrypt"])); +await t("empty usages", () => crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [])); +await t("verify usage only", async () => { const k = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-384" }, true, ["verify"]); return k.privateKey.usages + "|" + k.publicKey.usages; }); +await t("import wrong curve", () => crypto.subtle.importKey("spki", spki, { name: "ECDSA", namedCurve: "P-384" }, true, ["verify"])); +await t("import sign usage public", () => crypto.subtle.importKey("spki", spki, { name: "ECDSA", namedCurve: "P-256" }, true, ["sign"])); +await t("import garbage", () => crypto.subtle.importKey("spki", new Uint8Array([1, 2, 3]), { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"])); +await t("pss no salt", () => crypto.subtle.sign({ name: "RSA-PSS" }, pss.privateKey, data)); +await t("verify bad sig len", () => crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, ec.publicKey, new Uint8Array(10), data)); +await t("verify with private", () => crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, ec.privateKey, new Uint8Array(64), data)); +await t("sign hmac with ec", () => crypto.subtle.sign("HMAC", ec.privateKey, data)); +await t("sign ec hash obj", async () => (await crypto.subtle.sign({ name: "ECDSA", hash: { name: "SHA-512" } }, ec.privateKey, data)).byteLength); +await t("p521", async () => (await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, (await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-521" }, false, ["sign"])).privateKey, data)).byteLength); +await t("raw export p384", async () => (await crypto.subtle.exportKey("raw", (await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-384" }, true, ["sign","verify"])).publicKey)).byteLength); diff --git a/tests/diagnostics/webcrypto-refusals.ts b/tests/diagnostics/webcrypto-refusals.ts index d162872fbc..1d68813e8f 100644 --- a/tests/diagnostics/webcrypto-refusals.ts +++ b/tests/diagnostics/webcrypto-refusals.ts @@ -1,17 +1,7 @@ -// The native WebCrypto surface is HMAC importKey('raw'), sign and verify, -// and the SHA-1/SHA-2 digests, called directly on crypto.subtle. The 'jwk' -// key format and crypto.subtle or its methods taken as values are refused -// by name; the bundled declarations omit the other SubtleCrypto methods +// The native WebCrypto methods are called directly on crypto.subtle: +// crypto.subtle or its methods taken as values are refused by name; the +// bundled declarations omit the other SubtleCrypto methods // (node-types/fenced.ts pins their fence under @types/node). -const fromJwk = await crypto.subtle.importKey( - "jwk", - { kty: "oct", k: "c2VjcmV0" }, - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign"], -); -console.log(fromJwk.type); - const subtle = crypto.subtle; console.log(typeof subtle); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 16a74081fa..622f9cad39 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -69,11 +69,11 @@ http2.createSecureServer({ http2.createSecureServer({ allowHTTP1: true, cert: "pem", key: "pem", streamResetBurst: 1 + 0 }); http2.connect("https://localhost"); /* The crypto surface beyond the lowered slice (randomness, the hash - * chain, the introspection statics): asymmetric-key operations name the - * missing public-key stack, symmetric ciphers the missing cipher stack, - * prime checks theirs, and setFips the FIPS truth (getFips() lowers to 0). */ -import { checkPrimeSync, createCipheriv, generateKeyPair, setFips } from "node:crypto"; -generateKeyPair("rsa", { modulusLength: 2048 }, () => {}); + * chain, the introspection statics): Diffie-Hellman 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, createDiffieHellman, setFips } from "node:crypto"; +createDiffieHellman(512); createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); checkPrimeSync(7n); setFips(false); @@ -103,9 +103,9 @@ cipherStream.on("data", () => {}); import { createECDH } from "node:crypto"; createECDH("sect163k1"); createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); -/* WebCrypto beyond the HMAC slice: @types/node declares the whole - * SubtleCrypto surface, and the methods outside importKey, sign, verify - * and digest fence by name. */ +/* WebCrypto beyond the lowered methods: @types/node declares the whole + * SubtleCrypto surface, and the methods outside generateKey, importKey, + * exportKey, sign, verify and digest fence by name. */ async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); } diff --git a/tests/fixtures/node-types/keys.ts b/tests/fixtures/node-types/keys.ts new file mode 100644 index 0000000000..ba91f5d18e --- /dev/null +++ b/tests/fixtures/node-types/keys.ts @@ -0,0 +1,43 @@ +import { + type KeyObject, + createPrivateKey, + createPublicKey, + createSign, + createVerify, + generateKeyPair, + generateKeyPairSync, + sign, + verify, +} from "node:crypto"; + +const { publicKey, privateKey } = generateKeyPairSync("ec", { namedCurve: "P-256" }); +const data = Buffer.from("typed keys"); +const signature: Buffer = sign("sha256", data, privateKey); +console.log(publicKey.type, privateKey.asymmetricKeyType, verify("sha256", data, publicKey, signature)); +const pem = privateKey.export({ type: "pkcs8", format: "pem" }); +const parsed: KeyObject = createPrivateKey(pem); +console.log(parsed.equals(privateKey), createPublicKey(parsed).equals(publicKey)); +const pems = generateKeyPairSync("rsa", { + modulusLength: 1024, + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "pem" }, +}); +const signer = createSign("SHA256"); +signer.update("typed"); +const hex: string = signer.sign(pems.privateKey, "hex"); +const verifier = createVerify("SHA256"); +verifier.update("typed"); +console.log(pems.publicKey.split("\n")[0], verifier.verify(pems.publicKey, hex, "hex")); +const ed = generateKeyPairSync("ed25519"); +const edSignature: Buffer = sign(null, data, ed.privateKey); +console.log(ed.publicKey.asymmetricKeyType, edSignature.length, verify(null, data, ed.publicKey, edSignature)); +const edJwk = ed.publicKey.export({ format: "jwk" }); +console.log(edJwk.kty, edJwk.crv, createPublicKey({ key: edJwk, format: "jwk" }).equals(ed.publicKey)); +const edDer = generateKeyPairSync("ed25519", { + publicKeyEncoding: { type: "spki", format: "der" }, + privateKeyEncoding: { type: "pkcs8", format: "der" }, +}); +console.log(edDer.publicKey.length, edDer.privateKey.length); +generateKeyPair("rsa", { modulusLength: 1024 }, (err, rsaPublic, rsaPrivate) => { + console.log(err, rsaPublic.asymmetricKeyDetails?.modulusLength, rsaPrivate.type); +}); diff --git a/tests/fixtures/node-types/webcrypto-ecdsa.ts b/tests/fixtures/node-types/webcrypto-ecdsa.ts new file mode 100644 index 0000000000..2ec7cc4a76 --- /dev/null +++ b/tests/fixtures/node-types/webcrypto-ecdsa.ts @@ -0,0 +1,34 @@ +/* WebCrypto ECDSA, Ed25519 and RSA under @types/node: generateKey, exportKey, + * importKey, sign and verify lower to the same native values as under the + * bundled declarations. */ +import type { webcrypto } from "node:crypto"; + +async function main(): Promise { + const data = new TextEncoder().encode("node-types"); + const params: webcrypto.EcKeyGenParams = { name: "ECDSA", namedCurve: "P-384" }; + const pair: webcrypto.CryptoKeyPair = await crypto.subtle.generateKey(params, true, ["sign", "verify"]); + const ecdsa: webcrypto.EcdsaParams = { name: "ECDSA", hash: "SHA-384" }; + const signature = await crypto.subtle.sign(ecdsa, pair.privateKey, data); + console.log(pair.privateKey.type, signature.byteLength, await crypto.subtle.verify(ecdsa, pair.publicKey, signature, data)); + const spki = await crypto.subtle.exportKey("spki", pair.publicKey); + const imported = await crypto.subtle.importKey("spki", spki, { name: "ECDSA", namedCurve: "P-384" }, false, ["verify"]); + const algorithm = imported.algorithm as webcrypto.EcKeyAlgorithm; + console.log(algorithm.name, algorithm.namedCurve, await crypto.subtle.verify(ecdsa, imported, signature, data)); + const jwk: webcrypto.JsonWebKey = await crypto.subtle.exportKey("jwk", pair.privateKey); + console.log(jwk.kty, jwk.crv, jwk.key_ops?.join(",")); + const rsa = await crypto.subtle.generateKey( + { name: "RSA-PSS", modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, + false, + ["sign", "verify"], + ); + const pss: webcrypto.RsaPssParams = { name: "RSA-PSS", saltLength: 32 }; + const rsaSignature = await crypto.subtle.sign(pss, rsa.privateKey, data); + console.log(rsaSignature.byteLength, await crypto.subtle.verify(pss, rsa.publicKey, rsaSignature, data)); + const ed = (await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"])) as webcrypto.CryptoKeyPair; + const edSignature = await crypto.subtle.sign("Ed25519", ed.privateKey, data); + const raw = await crypto.subtle.exportKey("raw", ed.publicKey); + const edPublic = await crypto.subtle.importKey("raw", raw, "Ed25519", true, ["verify"]); + console.log(edPublic.algorithm.name, raw.byteLength, edSignature.byteLength, await crypto.subtle.verify("Ed25519", edPublic, edSignature, data)); +} + +void main(); diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 0bd13963d4..93a6872672 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -115,18 +115,18 @@ fenced.ts:69:71 - error SC2020: 'createSecureServer option 'streamResetBurst' wi hint: h2 session-tuning options are accepted with literal values but are not applied yet -fenced.ts:76:1 - error SC2020: 'crypto.generateKeyPair' is typed by @types/node but has no scriptc lowering yet +fenced.ts:76:1 - error SC2020: 'crypto.createDiffieHellman' is typed by @types/node but has no scriptc lowering yet - 75 | import { checkPrimeSync, createCipheriv, generateKeyPair, setFips } from "node:crypto"; - 76 | generateKeyPair("rsa", { modulusLength: 2048 }, () => {}); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 75 | import { checkPrimeSync, createCipheriv, createDiffieHellman, setFips } from "node:crypto"; + 76 | createDiffieHellman(512); + | ^~~~~~~~~~~~~~~~~~~~~~~~ 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); - hint: asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics + hint: Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH fenced.ts:77:16 - error SC2020: 'crypto.createCipheriv with cipher 'aes-128-ccm'' is part of the standard library types but has no scriptc lowering yet - 76 | generateKeyPair("rsa", { modulusLength: 2048 }, () => {}); + 76 | createDiffieHellman(512); 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); | ^~~~~~~~~~~~~ 78 | checkPrimeSync(7n); @@ -228,7 +228,7 @@ fenced.ts:105:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer 104 | createECDH("sect163k1"); 105 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 106 | /* WebCrypto beyond the HMAC slice: @types/node declares the whole + 106 | /* WebCrypto beyond the lowered methods: @types/node declares the whole hint: Node reads the second argument as the key's input encoding and answers a Buffer; pass the output encoding third: computeSecret(key, undefined, encoding) @@ -239,7 +239,7 @@ fenced.ts:110:9 - error SC2020: 'crypto.subtle.encrypt' is part of the standard | ^~~~~~~~~~~~~~~~~~~~~ 111 | } - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle fenced.ts:115:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet diff --git a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt index 521edff3ce..2821d8306c 100644 --- a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt +++ b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt @@ -1,26 +1,17 @@ -webcrypto-refusals.ts:7:3 - error SC2020: 'crypto.subtle.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet +webcrypto-refusals.ts:5:16 - error SC2020: 'crypto.subtle as a value' is part of the standard library types but has no scriptc lowering yet - 6 | const fromJwk = await crypto.subtle.importKey( - 7 | "jwk", - | ^~~~~ - 8 | { kty: "oct", k: "c2VjcmV0" }, + 4 | // (node-types/fenced.ts pins their fence under @types/node). + 5 | const subtle = crypto.subtle; + | ^~~~~~~~~~~~~ + 6 | console.log(typeof subtle); - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle -webcrypto-refusals.ts:15:16 - error SC2020: 'crypto.subtle as a value' is part of the standard library types but has no scriptc lowering yet +webcrypto-refusals.ts:8:20 - error SC2020: 'crypto.subtle.sign as a value' is part of the standard library types but has no scriptc lowering yet - 14 | - 15 | const subtle = crypto.subtle; - | ^~~~~~~~~~~~~ - 16 | console.log(typeof subtle); + 7 | + 8 | console.log(typeof crypto.subtle.sign); + | ^~~~~~~~~~~~~~~~~~ + 9 | - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle - -webcrypto-refusals.ts:18:20 - error SC2020: 'crypto.subtle.sign as a value' is part of the standard library types but has no scriptc lowering yet - - 17 | - 18 | console.log(typeof crypto.subtle.sign); - | ^~~~~~~~~~~~~~~~~~ - 19 | - - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file diff --git a/tests/harness/corpus-costs/7080-crypto-ed25519.mjs.json b/tests/harness/corpus-costs/7080-crypto-ed25519.mjs.json new file mode 100644 index 0000000000..8a1e96b689 --- /dev/null +++ b/tests/harness/corpus-costs/7080-crypto-ed25519.mjs.json @@ -0,0 +1 @@ +{ "plain": 4906, "san": 50709 } diff --git a/tests/harness/corpus-costs/7081-webcrypto-ed25519.mjs.json b/tests/harness/corpus-costs/7081-webcrypto-ed25519.mjs.json new file mode 100644 index 0000000000..47b28eca21 --- /dev/null +++ b/tests/harness/corpus-costs/7081-webcrypto-ed25519.mjs.json @@ -0,0 +1 @@ +{ "plain": 7958, "san": 51204 } diff --git a/tests/harness/corpus-costs/7082-crypto-key-objects.ts.json b/tests/harness/corpus-costs/7082-crypto-key-objects.ts.json new file mode 100644 index 0000000000..b0d8e50c9f --- /dev/null +++ b/tests/harness/corpus-costs/7082-crypto-key-objects.ts.json @@ -0,0 +1 @@ +{ "plain": 4405, "san": 7280 } diff --git a/tests/harness/corpus-costs/7083-crypto-sign-verify.ts.json b/tests/harness/corpus-costs/7083-crypto-sign-verify.ts.json new file mode 100644 index 0000000000..6503939825 --- /dev/null +++ b/tests/harness/corpus-costs/7083-crypto-sign-verify.ts.json @@ -0,0 +1 @@ +{ "plain": 3631, "san": 5127 } diff --git a/tests/harness/corpus-costs/7084-crypto-key-pairs.cjs.json b/tests/harness/corpus-costs/7084-crypto-key-pairs.cjs.json new file mode 100644 index 0000000000..363a8de390 --- /dev/null +++ b/tests/harness/corpus-costs/7084-crypto-key-pairs.cjs.json @@ -0,0 +1 @@ +{ "plain": 6267, "san": 19680 } diff --git a/tests/harness/corpus-costs/7085-webcrypto-ecdsa-rsa.ts.json b/tests/harness/corpus-costs/7085-webcrypto-ecdsa-rsa.ts.json new file mode 100644 index 0000000000..73c07085d4 --- /dev/null +++ b/tests/harness/corpus-costs/7085-webcrypto-ecdsa-rsa.ts.json @@ -0,0 +1 @@ +{ "plain": 5184, "san": 40791 } diff --git a/tests/harness/corpus-costs/7086-webcrypto-ecdsa-rsa-errors.mjs.json b/tests/harness/corpus-costs/7086-webcrypto-ecdsa-rsa-errors.mjs.json new file mode 100644 index 0000000000..a74af7ae12 --- /dev/null +++ b/tests/harness/corpus-costs/7086-webcrypto-ecdsa-rsa-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 8513, "san": 13054 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 8af6998ca6..de73c62898 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1641,7 +1641,7 @@ console.log(typeof promisePrototype, typeof promisePrototype.then, promiseProtot // Node's own rejections (unknown names, bad usages) are corpus-pinned // (6292, 6294); these refusals have no Node twin, so they live here. describe("WebCrypto refusals (JS lane)", () => { - test("recognized algorithms outside the HMAC slice reject with SC2020", async () => { + test("recognized algorithms outside the lowered ones reject with SC2020", async () => { const r = await compileAndRun( "webcrypto-unsupported-algorithms", `const bytes = new TextEncoder().encode("secret"); @@ -1653,11 +1653,10 @@ async function attempt(label, run) { console.log(label, e.name, e.code, e.message); } } -await attempt("rsa", () => - crypto.subtle.importKey("raw", bytes, { name: "RSA-PSS", hash: "SHA-256" }, false, ["verify"]), -); +await attempt("aes", () => crypto.subtle.importKey("raw", bytes, { name: "AES-GCM" }, false, ["encrypt"])); const key = await crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); -await attempt("ecdsa", () => crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, bytes)); +await attempt("ed448", () => crypto.subtle.sign({ name: "Ed448" }, key, bytes)); +await attempt("ecdsa with an hmac key", () => crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, bytes)); await attempt("sha3", () => crypto.subtle.digest("SHA3-256", bytes)); await attempt("sha3 hmac", () => crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA3-256" }, false, ["sign"]), @@ -1672,15 +1671,16 @@ await attempt("computed jwk", () => expect(r.exitCode).toBe(0); expect(r.stderr).toBe(""); expect(r.stdout).toBe( - "rsa Error SC2020 crypto.subtle.importKey with the RSA-PSS algorithm is not supported yet\n" + - "ecdsa Error SC2020 crypto.subtle.sign with the ECDSA algorithm is not supported yet\n" + + "aes Error SC2020 crypto.subtle.importKey with the AES-GCM algorithm is not supported yet\n" + + "ed448 Error SC2020 crypto.subtle.sign with the Ed448 algorithm is not supported yet\n" + + "ecdsa with an hmac key InvalidAccessError 15 Key algorithm mismatch\n" + "sha3 Error SC2020 crypto.subtle.digest with the SHA3-256 algorithm is not supported yet\n" + "sha3 hmac Error SC2020 crypto.subtle.importKey with the SHA3-256 algorithm is not supported yet\n" + "computed jwk Error SC2020 crypto.subtle.importKey with the 'jwk' key format is not supported yet\n", ); }); - test("a literal 'jwk' import is a named fence where it runs", async () => { + test("a literal 'jwk' HMAC import rejects with SC2020", async () => { const r = await compileAndRun( "webcrypto-jwk-literal", `console.log("before"); @@ -1690,8 +1690,59 @@ await crypto.subtle.importKey("jwk", { kty: "oct", k: "c2VjcmV0" }, { name: "HMA ); expect(r.exitCode).toBe(1); expect(r.stdout).toBe("before\n"); - expect(r.stderr).toMatch( - /^Uncaught Error: 'crypto\.subtle\.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet.*\n$/, + expect(r.stderr).toBe( + "Uncaught Error: crypto.subtle.importKey with the 'jwk' key format is not supported yet [SC2020]\n", + ); + }); +}); + +// The Ed25519 arithmetic (scr_ed25519.c) links only into programs whose +// string literals name Ed25519. Without it, an Ed25519 public key still +// imports and exports, while private-key parsing, signing and verification +// reject with a coded SC2020 Error where Node answers; there is no Node +// twin, so this lives here. The corpus (7080, 7081) pins the linked path. +describe("Ed25519 without the Ed25519 unit (JS lane)", () => { + test("a program that never names Ed25519 refuses its arithmetic with SC2020", async () => { + const r = await compileAndRun( + "ed25519-unit-not-linked", + `import * as c from "node:crypto"; +const spki = Buffer.from("302a300506032b6570032100d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", "hex"); +const pkcs8 = Buffer.from("302e020100300506032b6570042204209d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", "hex"); +const pub = c.createPublicKey({ key: spki, format: "der", type: "spki" }); +console.log(pub.asymmetricKeyType, pub.export({ format: "jwk" }).x); +async function attempt(label, run) { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +await attempt("pkcs8", () => c.createPrivateKey({ key: pkcs8, format: "der", type: "pkcs8" })); +await attempt("verify", () => c.verify(null, Buffer.from("x"), pub, Buffer.alloc(64))); +// ECDSA links the WebCrypto key unit; the Ed25519 name is built at run time. +const ecdsa = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]); +console.log(ecdsa.privateKey.type); +const name = ["Ed", "25519"].join(""); +await attempt("generateKey", () => crypto.subtle.generateKey(name, false, ["sign", "verify"])); +await attempt("importKey", () => crypto.subtle.importKey("pkcs8", pkcs8, name, false, ["sign"])); +const raw = await crypto.subtle.importKey("raw", spki.subarray(12), name, false, ["verify"]); +await attempt("subtle verify", () => crypto.subtle.verify(name, raw, new Uint8Array(64), new Uint8Array(1))); +`, + "mjs", + ); + const refusal = + "Error SC2020 Ed25519 keys are supported when a string literal in the program names 'ed25519'"; + expect(r.exitCode).toBe(0); + expect(r.stderr).toBe(""); + expect(r.stdout).toBe( + "ed25519 11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\n" + + `pkcs8 ${refusal}\n` + + `verify ${refusal}\n` + + "private\n" + + `generateKey ${refusal}\n` + + `importKey ${refusal}\n` + + `subtle verify ${refusal}\n`, ); }); }); diff --git a/tests/harness/library-asyncfree.test.ts b/tests/harness/library-asyncfree.test.ts index 7c4d029cc3..d924f26d11 100644 --- a/tests/harness/library-asyncfree.test.ts +++ b/tests/harness/library-asyncfree.test.ts @@ -97,6 +97,15 @@ describe("async_free detection over the IR", () => { ).toContain("child_process"); }); + test("asymmetric keys and signatures refuse", async () => { + expect( + await surfaceOf( + "pki", + `import * as crypto from "node:crypto";\nconst { privateKey } = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });\nconsole.log(crypto.sign("sha256", Buffer.from("x"), privateKey).length > 0);\n`, + ), + ).toContain("asymmetric KeyObjects"); + }); + test("process signal listeners refuse", async () => { expect( await surfaceOf( diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index dabb9b0bfc..186ded4755 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -1114,6 +1114,38 @@ test("node-types: ECDH matches Node under @types/node", async () => { expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: asymmetric keys and signatures match Node under @types/node", async () => { + const outDir = outDirFor("node-keys"); + const entry = join(nodeTypesDir, "keys.ts"); + const result = await compile(entry, { outPath: join(outDir, "keys"), outDir, sanitize }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + +test("node-types: WebCrypto ECDSA, Ed25519 and RSA match Node under @types/node", async () => { + const outDir = outDirFor("node-webcrypto-ecdsa"); + const entry = join(nodeTypesDir, "webcrypto-ecdsa.ts"); + const result = await compile(entry, { + outPath: join(outDir, "webcrypto-ecdsa"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: WebCrypto HMAC matches Node under @types/node", async () => { const outDir = outDirFor("node-webcrypto-hmac"); const entry = join(nodeTypesDir, "webcrypto-hmac.ts"); diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 7da7626b7f..d1dc52c7ac 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -48,6 +48,8 @@ const features: NativeLinkFeatures = { tlsCa: false, cipher: false, ecdh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, }; diff --git a/tests/harness/self-hosting-runtime-pack.test.ts b/tests/harness/self-hosting-runtime-pack.test.ts index f4fde795c0..908bdc5bb4 100644 --- a/tests/harness/self-hosting-runtime-pack.test.ts +++ b/tests/harness/self-hosting-runtime-pack.test.ts @@ -49,6 +49,8 @@ const base: NativeLinkFeatures = { tlsCa: false, cipher: false, ecdh: false, + pki: false, + ed25519: false, webcrypto: false, perfLoop: false, }; From 1773ffbef35e8ffb2cdfb060278ac316df85116d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:52:01 -0500 Subject: [PATCH 081/395] WebCrypto: the crypto global, node:crypto's webcrypto, and secret-key SubtleCrypto --- .../limitations/builtin-apis/crypto.mdx | 3 + .../src/generated/node-v24-compatibility.json | 58 +- .../generated/node-v24-internal.json | 198 ++- internal/compatibility/static-support.json | 11 +- .../ambient/scriptc-node-fallback.d.ts | 103 +- .../compiler/src/backend/llvm/lib-shared.ts | 12 + .../src/backend/native-link-info-core.ts | 5 +- .../compiler/src/backend/native/contracts.ts | 5 +- .../compiler/src/backend/native/executable.ts | 3 + .../src/backend/target-diagnostics.ts | 8 + .../src/frontend/lowering/builtins/crypto.ts | 5 +- .../frontend/lowering/builtins/webcrypto.ts | 60 +- .../lowering/expressions/identifiers.ts | 5 +- .../lowering/expressions/property-access.ts | 14 +- .../src/frontend/lowering/lower-calls.ts | 35 +- .../src/frontend/lowering/lower-stmts.ts | 10 +- .../compiler/src/frontend/lowering/lowerer.ts | 4 + .../src/frontend/lowering/surfaces.ts | 20 +- packages/compiler/src/frontend/type-mapper.ts | 10 + .../compiler/src/ir/builtin-signatures.ts | 7 + packages/compiler/src/ir/runtime-features.ts | 14 +- packages/compiler/surface-manifest.json | 12 +- .../corpus/7120-webcrypto-random-uuid.ts.json | 6 + .../7121-webcrypto-crypto-object.mjs.json | 6 + .../corpus/7122-webcrypto-node-crypto.ts.json | 6 + .../7123-webcrypto-node-crypto.cjs.json | 6 + .../corpus/7124-webcrypto-secret-keys.ts.json | 6 + .../corpus/7125-webcrypto-key-errors.mjs.json | 6 + .../tests/corpus/7126-webcrypto-aes.ts.json | 6 + .../corpus/7127-webcrypto-aes-errors.mjs.json | 6 + .../corpus/7128-webcrypto-derive.ts.json | 6 + .../7129-webcrypto-derive-errors.mjs.json | 6 + .../node-types/webcrypto-keys.ts.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 3 +- .../runtime-pack-matrix.mjs | 4 + .../runtime-pack-common/scripts/build.mjs | 3 +- .../runtime-wasm32-wasi/scripts/build.mjs | 1 + packages/runtime/src/scr_crypto.h | 44 +- packages/runtime/src/scr_webcrypto.c | 1362 +++++++++++++++-- packages/runtime/src/scr_webcrypto_aes.c | 137 ++ packages/runtime/src/scr_webcrypto_object.c | 53 + tests/corpus/7120-webcrypto-random-uuid.ts | 22 + tests/corpus/7121-webcrypto-crypto-object.mjs | 35 + tests/corpus/7122-webcrypto-node-crypto.ts | 38 + tests/corpus/7123-webcrypto-node-crypto.cjs | 31 + tests/corpus/7124-webcrypto-secret-keys.ts | 91 ++ tests/corpus/7125-webcrypto-key-errors.mjs | 95 ++ tests/corpus/7126-webcrypto-aes.ts | 77 + tests/corpus/7127-webcrypto-aes-errors.mjs | 71 + tests/corpus/7128-webcrypto-derive.ts | 45 + tests/corpus/7129-webcrypto-derive-errors.mjs | 72 + tests/diagnostics/webcrypto-refusals.ts | 19 +- tests/fixtures/node-types/fenced.ts | 12 +- tests/fixtures/node-types/webcrypto-keys.ts | 32 + .../__snapshots__/node-types-fenced.txt | 10 +- .../__snapshots__/webcrypto-refusals.ts.txt | 31 +- .../7120-webcrypto-random-uuid.ts.json | 1 + .../7121-webcrypto-crypto-object.mjs.json | 1 + .../7122-webcrypto-node-crypto.ts.json | 1 + .../7123-webcrypto-node-crypto.cjs.json | 1 + .../7124-webcrypto-secret-keys.ts.json | 1 + .../7125-webcrypto-key-errors.mjs.json | 1 + .../corpus-costs/7126-webcrypto-aes.ts.json | 1 + .../7127-webcrypto-aes-errors.mjs.json | 1 + .../7128-webcrypto-derive.ts.json | 1 + .../7129-webcrypto-derive-errors.mjs.json | 1 + tests/harness/errors.test.ts | 31 +- tests/harness/project-config.test.ts | 18 + 68 files changed, 2683 insertions(+), 332 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7120-webcrypto-random-uuid.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7121-webcrypto-crypto-object.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-webcrypto-node-crypto.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-webcrypto-node-crypto.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-webcrypto-secret-keys.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-webcrypto-key-errors.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-webcrypto-aes.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-webcrypto-aes-errors.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-webcrypto-derive.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7129-webcrypto-derive-errors.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-keys.ts.json create mode 100644 packages/runtime/src/scr_webcrypto_aes.c create mode 100644 packages/runtime/src/scr_webcrypto_object.c create mode 100644 tests/corpus/7120-webcrypto-random-uuid.ts create mode 100644 tests/corpus/7121-webcrypto-crypto-object.mjs create mode 100644 tests/corpus/7122-webcrypto-node-crypto.ts create mode 100644 tests/corpus/7123-webcrypto-node-crypto.cjs create mode 100644 tests/corpus/7124-webcrypto-secret-keys.ts create mode 100644 tests/corpus/7125-webcrypto-key-errors.mjs create mode 100644 tests/corpus/7126-webcrypto-aes.ts create mode 100644 tests/corpus/7127-webcrypto-aes-errors.mjs create mode 100644 tests/corpus/7128-webcrypto-derive.ts create mode 100644 tests/corpus/7129-webcrypto-derive-errors.mjs create mode 100644 tests/fixtures/node-types/webcrypto-keys.ts create mode 100644 tests/harness/corpus-costs/7120-webcrypto-random-uuid.ts.json create mode 100644 tests/harness/corpus-costs/7121-webcrypto-crypto-object.mjs.json create mode 100644 tests/harness/corpus-costs/7122-webcrypto-node-crypto.ts.json create mode 100644 tests/harness/corpus-costs/7123-webcrypto-node-crypto.cjs.json create mode 100644 tests/harness/corpus-costs/7124-webcrypto-secret-keys.ts.json create mode 100644 tests/harness/corpus-costs/7125-webcrypto-key-errors.mjs.json create mode 100644 tests/harness/corpus-costs/7126-webcrypto-aes.ts.json create mode 100644 tests/harness/corpus-costs/7127-webcrypto-aes-errors.mjs.json create mode 100644 tests/harness/corpus-costs/7128-webcrypto-derive.ts.json create mode 100644 tests/harness/corpus-costs/7129-webcrypto-derive-errors.mjs.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 3c830c63d9..b00ea438ef 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -16,4 +16,7 @@ A call on an ECDH value whose arguments no @types/node overload accepts (`getPublicKey("HEX")`) is refused at compile time, as is `computeSecret(view, encoding)`: Node reads that encoding as the input encoding and answers a Buffer where @types/node declares a string. ECDH property reads and writes throw a coded SC2020 error. Other public-key operations remain unsupported. + The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript. + Its `subtle` object, also node:crypto's `subtle` export, supports `generateKey()` and `exportKey()` for HMAC and AES keys, `importKey()` for HMAC, AES, PBKDF2, and HKDF keys, `encrypt()` and `decrypt()` with AES-GCM, AES-CBC, and AES-CTR on native targets, `deriveBits()` and `deriveKey()` with PBKDF2 and HKDF, HMAC `sign()` and `verify()`, and SHA-1 and SHA-2 `digest()`. + Secret keys import and export in the `raw`, `raw-secret`, and `jwk` formats; other SubtleCrypto methods, algorithms, and key formats are refused. Native derivations finish before their deferred callbacks, so computation blocks the caller. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 66b0ba3dd3..2c09eb9be2 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -12442,9 +12442,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 by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -12465,9 +12465,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -79656,9 +79656,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 documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -79680,7 +79680,7 @@ }, "static": { "status": "supported", - "detail": "Implemented by a dedicated static compiler/runtime path.", + "detail": "Implemented for the documented scriptc module-loader subset.", "verification": "test-backed" } }, @@ -79703,7 +79703,7 @@ }, "static": { "status": "supported", - "detail": "Implemented by a dedicated static compiler/runtime path.", + "detail": "Implemented for the documented scriptc module-loader subset.", "verification": "test-backed" } }, @@ -80001,9 +80001,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 documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80024,9 +80024,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 documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80047,9 +80047,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 documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80139,9 +80139,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 documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80162,9 +80162,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 documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80208,9 +80208,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 documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index c6a7a102c3..c9f6dc6be6 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19313,6 +19313,8 @@ "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", + "tests/corpus/7122-webcrypto-node-crypto.ts", + "tests/corpus/7123-webcrypto-node-crypto.cjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -21215,8 +21217,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:subtle", + "tests": [ + "tests/corpus/7122-webcrypto-node-crypto.ts", + "tests/corpus/7123-webcrypto-node-crypto.cjs" + ] }, "anchorSource": "ancestor" }, @@ -21238,8 +21244,12 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.webcrypto" + "status": "partial", + "evidence": "compiler-dedicated:crypto.webcrypto", + "tests": [ + "tests/corpus/7122-webcrypto-node-crypto.ts", + "tests/corpus/7123-webcrypto-node-crypto.cjs" + ] }, "anchorSource": "exact" }, @@ -106607,8 +106617,6 @@ "tests/corpus/1534-crypto-hash-chain.ts", "tests/corpus/2556-crypto-introspection.ts", "tests/corpus/2890-crypto-static-utility.ts", - "tests/corpus/5442-crypto-random-values.ts", - "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", @@ -106616,11 +106624,20 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/harness/npm-static.test.ts" ] }, @@ -110078,8 +110095,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.Crypto", + "tests": [ + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", + "tests/corpus/6293-webcrypto-digest.ts" + ] }, "anchorSource": "exact" }, @@ -110102,8 +110124,10 @@ }, "static": { "status": "supported", - "evidence": "compiler-dedicated:crypto.getRandomValues", + "evidence": "compiler-feature:webcrypto.crypto.getRandomValues", "tests": [ + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js" ] @@ -110129,11 +110153,12 @@ }, "static": { "status": "supported", - "evidence": "compiler-dedicated:crypto.randomUUID", + "evidence": "compiler-feature:webcrypto.crypto.randomUUID", "tests": [ - "tests/corpus/5442-crypto-random-values.ts", - "tests/corpus/5443-crypto-random-arguments.js", - "tests/corpus/1358-crypto-random.ts" + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", + "tests/corpus/6204-crypto-get-random-values.ts", + "tests/corpus/6205-crypto-get-random-values.js" ] }, "anchorSource": "exact" @@ -110189,6 +110214,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.CryptoKey", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110217,6 +110243,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.cryptoKey.algorithm", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110245,6 +110272,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.extractable", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110273,6 +110301,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.type", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110301,6 +110330,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.usages", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110471,8 +110501,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.decrypt", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110494,8 +110539,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.deriveBits", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110517,8 +110577,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.deriveKey", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110543,6 +110618,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.digest", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", @@ -110617,8 +110699,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.encrypt", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110640,8 +110737,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.exportKey", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110686,8 +110798,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.generateKey", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110712,6 +110839,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.importKey", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", @@ -110743,6 +110877,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.sign", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", @@ -110797,6 +110938,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.verify", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 23c7a159b6..e7b3486c4e 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -187,16 +187,18 @@ "stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"], "stdlib.headers": ["tests/harness/fetch-conformance.test.ts"], "stdlib.request": ["tests/harness/fetch-conformance.test.ts"], - "stdlib.webcrypto.subtle": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], - "stdlib.webcrypto.CryptoKey": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], + "stdlib.webcrypto.subtle": ["tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], + "stdlib.webcrypto.CryptoKey": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], "stdlib.response": ["tests/harness/fetch-conformance.test.ts"], "stdlib.readable-stream": ["tests/harness/fetch-conformance.test.ts"], "stdlib.abort": ["tests/harness/fetch-conformance.test.ts"] }, "features": [ - { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, + { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, { "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, - { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "webcrypto", "signatures": ["Class: `Crypto`"], "status": "partial", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6293-webcrypto-digest.ts"] }, + { "chapter": "webcrypto", "symbols": ["crypto.randomUUID", "crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] }, { "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] }, { "chapter": "globals", "signatures": ["`userAgent` Type: {string}"], "status": "supported", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts"] }, { "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `SubtleCrypto`"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, @@ -413,6 +415,7 @@ { "symbols": ["assert.deepEqual", "assert.notDeepEqual"], "status": "partial", "evidence": ["tests/corpus/5513-assert-deep-equal-loose.js", "tests/corpus/5514-assert-deep-equal-typed.ts", "tests/corpus/5516-assert-deep-equal-edges.js"] }, { "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] }, { "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["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/fixtures/node-types/cipher.ts"] }, + { "chapter": "crypto", "symbols": ["crypto.webcrypto", "subtle"], "status": "partial", "evidence": ["tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs"] }, { "chapter": "crypto", "symbols": ["ECDH", "ecdh.computeSecret", "ecdh.generateKeys", "ecdh.getPrivateKey", "ecdh.getPublicKey", "ecdh.setPrivateKey", "ecdh.setPublicKey"], "status": "partial", "evidence": ["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"] }, { "chapter": "errors", "symbols": ["ERR_CRYPTO_ECDH_INVALID_FORMAT", "ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY"], "status": "partial", "evidence": ["tests/corpus/6702-ecdh-errors.js"] }, { "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index dbb3cd8fda..489af17004 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -544,12 +544,15 @@ declare var DOMException: { readonly prototype: DOMException; }; -/* WebCrypto's HMAC slice (scr_webcrypto.c): crypto.subtle's importKey for - * raw HMAC keys, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as +/* WebCrypto's secret-key slice (scr_webcrypto.c): crypto.subtle's + * importKey, generateKey and exportKey for HMAC and AES keys (importKey + * also for PBKDF2 and HKDF keys), AES-GCM/CBC/CTR encrypt and decrypt, + * PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as * exactly that surface (the declarations mirror @types/node's webcrypto - * shapes). Every other SubtleCrypto method and crypto member is absent, - * so a use is an honest type error. CryptoKey values are native handles - * whose four members read through the handle. */ + * shapes), plus the global's randomUUID and getRandomValues. Every other + * SubtleCrypto method and crypto member is absent, so a use is an honest + * type error. CryptoKey values are native handles whose four members read + * through the handle. */ declare var crypto: Crypto; /* The Crypto and SubtleCrypto constructors (feature probes only) stay * `unknown`; the values keep this declaration order. */ @@ -582,9 +585,44 @@ interface HmacImportParams extends Algorithm { hash: HashAlgorithmIdentifier; length?: number; } +interface HmacKeyGenParams extends Algorithm { + hash: HashAlgorithmIdentifier; + length?: number; +} +interface AesKeyGenParams extends Algorithm { + length: number; +} +interface AesGcmParams extends Algorithm { + additionalData?: BufferSource; + iv: BufferSource; + tagLength?: number; +} +interface AesCbcParams extends Algorithm { + iv: BufferSource; +} +interface AesCtrParams extends Algorithm { + counter: BufferSource; + length: number; +} +interface Pbkdf2Params extends Algorithm { + hash: HashAlgorithmIdentifier; + iterations: number; + salt: BufferSource; +} +interface HkdfParams extends Algorithm { + hash: HashAlgorithmIdentifier; + info: BufferSource; + salt: BufferSource; +} +interface AesDerivedKeyParams extends Algorithm { + length: number; +} interface KeyAlgorithm { name: string; } +interface AesKeyAlgorithm extends KeyAlgorithm { + length: number; +} interface HmacKeyAlgorithm extends KeyAlgorithm { hash: KeyAlgorithm; length: number; @@ -623,10 +661,37 @@ interface JsonWebKey { x?: string; y?: string; } -/* The 'jwk' form type-checks like @types/node's; the compiler refuses it - * (the JsonWebKey import is not implemented). */ interface SubtleCrypto { + decrypt( + algorithm: AesGcmParams | AesCbcParams | AesCtrParams, + key: CryptoKey, + data: BufferSource, + ): Promise; + deriveBits( + algorithm: Pbkdf2Params | HkdfParams, + baseKey: CryptoKey, + length?: number | null, + ): Promise; + deriveKey( + algorithm: Pbkdf2Params | HkdfParams, + baseKey: CryptoKey, + derivedKeyType: AlgorithmIdentifier | AesDerivedKeyParams | HmacImportParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise; + encrypt( + algorithm: AesGcmParams | AesCbcParams | AesCtrParams, + key: CryptoKey, + data: BufferSource, + ): Promise; + exportKey(format: "jwk", key: CryptoKey): Promise; + exportKey(format: Exclude, key: CryptoKey): Promise; + generateKey( + algorithm: AesKeyGenParams | HmacKeyGenParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; importKey( format: "jwk", keyData: JsonWebKey, @@ -651,6 +716,21 @@ interface SubtleCrypto { } interface Crypto { readonly subtle: SubtleCrypto; + getRandomValues< + T extends + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | BigInt64Array + | BigUint64Array, + >( + typedArray: T, + ): T; + randomUUID(): `${string}-${string}-${string}-${string}-${string}`; } /* Capability-gated globals harness code probes behind feature guards @@ -2784,8 +2864,7 @@ declare module "crypto" { b: ArrayBufferView | ArrayBufferLike, ): boolean; /* WebCrypto's getRandomValues (also the webcrypto object's): integer - * typed arrays, filled in place and returned. The crypto GLOBAL stays - * unknown in fallback builds. */ + * typed arrays, filled in place and returned. */ type RandomValuesArray = | Int8Array | Uint8Array @@ -2797,9 +2876,9 @@ declare module "crypto" { | BigInt64Array | BigUint64Array; export function getRandomValues(typedArray: T): T; - export const webcrypto: { - getRandomValues(typedArray: T): T; - }; + /* The WebCrypto global's object and its SubtleCrypto. */ + export const webcrypto: Crypto; + export const subtle: SubtleCrypto; export function randomFillSync( buffer: T, offset?: number, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8bb8bc3aab..5076246523 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -615,10 +615,17 @@ export const LIB_FN_SYMS: Record = { "crypto.keyObjectInput": "scr_crypto_key_object_input", "crypto.isKeyObject": "scr_crypto_is_key_object", "webcrypto.importKey": "scr_webcrypto_import_key", + "webcrypto.generateKey": "scr_webcrypto_generate_key", + "webcrypto.exportKey": "scr_webcrypto_export_key", + "webcrypto.encrypt": "scr_webcrypto_encrypt", + "webcrypto.decrypt": "scr_webcrypto_decrypt", + "webcrypto.deriveBits": "scr_webcrypto_derive_bits", + "webcrypto.deriveKey": "scr_webcrypto_derive_key", "webcrypto.sign": "scr_webcrypto_sign", "webcrypto.verify": "scr_webcrypto_verify", "webcrypto.digest": "scr_webcrypto_digest", "webcrypto.isCryptoKey": "scr_webcrypto_is_crypto_key", + "webcrypto.object": "scr_webcrypto_object", "crypto.randomUUIDv7": "scr_crypto_random_uuid_v7", "crypto.pbkdf2": "scr_crypto_pbkdf2", "crypto.hkdf": "scr_crypto_hkdf", @@ -1573,6 +1580,11 @@ export const USES_TIMERS_LIB_FNS = new Set([ // ecdh.setPublicKey reports DEP0031 on the next tick. "crypto.ecdhNew", // WebCrypto results settle through the deferred crypto queue. + "webcrypto.generateKey", + "webcrypto.encrypt", + "webcrypto.decrypt", + "webcrypto.deriveBits", + "webcrypto.deriveKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index a476e1f7f2..55301948ba 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -84,8 +84,9 @@ export interface NativeLinkFeatures { foreignFfi: boolean; nodeTest: boolean; tls: boolean; - /** crypto.createCipheriv/createDecipheriv: scr_cipher.c plus the mbedTLS - * archive. */ + /** crypto.createCipheriv/createDecipheriv and WebCrypto's encrypt and + * decrypt: scr_cipher.c plus the mbedTLS archive (and, with webcrypto, + * scr_webcrypto_aes.c). */ cipher: boolean; /** crypto.createECDH and ECDH.convertKey: scr_ecdh.c plus the mbedTLS * archive. */ diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 06a41c9b22..8df233585d 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -243,8 +243,9 @@ export interface CcOptions { * The unit also compiles whenever `tls` does — scr_tls.c consults its * default-set override and shared Windows-certificate enumerator. */ tlsCa?: boolean; - /** crypto.createCipheriv/createDecipheriv: compiles scr_cipher.c and links - * the mbedTLS archive (shared with tls). */ + /** crypto.createCipheriv/createDecipheriv and WebCrypto's encrypt and + * decrypt: compiles scr_cipher.c (and, with webcrypto, + * scr_webcrypto_aes.c) and links the mbedTLS archive (shared with tls). */ cipher?: boolean; /** crypto.createECDH and ECDH.convertKey: compiles scr_ecdh.c and links * the mbedTLS archive (shared with tls). */ diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index a551ad600d..229b2f9671 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -297,6 +297,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"), unit((u) => u.opts.nodeTest, "scr_test.c"), unit((u) => u.opts.webcrypto, "scr_webcrypto.c"), + // The crypto global's object extends scr_async_dyn.c's. + unit((u) => u.opts.webcrypto && u.opts.dynAsync, "scr_webcrypto_object.c"), // The CA-store unit rides its own gate OR the tls one: scr_tls.c // references its default-set override unconditionally. unit((u) => u.tlsCa, "scr_tls_ca.c"), @@ -308,6 +310,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ ...(u.tls ? [u.rt(join(u.rtDir, "scr_tls.c"))] : []), ...(u.cipher ? [u.rt(join(u.rtDir, "scr_cipher.c"))] : []), ...(u.ecdh ? [u.rt(join(u.rtDir, "scr_ecdh.c"))] : []), + ...(u.cipher && u.opts.webcrypto ? [u.rt(join(u.rtDir, "scr_webcrypto_aes.c"))] : []), u.tlsArchive, // mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h). // The unconditional win32 libs above do not carry it. diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts index b7049f6fe3..eb70a0d2c5 100644 --- a/packages/compiler/src/backend/target-diagnostics.ts +++ b/packages/compiler/src/backend/target-diagnostics.ts @@ -208,6 +208,14 @@ export function moduleWasiUnavailableSurface( }; return false; } + if (node.fn === "webcrypto.encrypt" || node.fn === "webcrypto.decrypt") { + found = { + surface: + "WebCrypto encrypt and decrypt (the wasm32-wasi runtime does not include the mbedTLS library)", + loc, + }; + return false; + } if (node.fn === "crypto.ecdhNew" || node.fn === "crypto.ecdhConvertKey") { found = { surface: "ECDH (the wasm32-wasi runtime does not include the mbedTLS library)", diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index f4270dbe82..fc1502958c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -7,6 +7,7 @@ import { type Lowerer, own } from "../lowerer.js"; import { locOf } from "../../program.js"; import { CIPHER_TYPE_NAMES } from "../../type-mapper.js"; import { CRYPTO_CIPHERS, CRYPTO_CURVES, CRYPTO_HASHES } from "../crypto-tables.js"; +import { stdlibGlobalNameOf } from "../surfaces.js"; import { BOOL, BYTES_U8, @@ -1316,8 +1317,10 @@ export function lowerCryptoGetRandomValues( } /** Whether `receiver` names a WebCrypto Crypto object: the `crypto` - * global (also through globalThis) or node:crypto's `webcrypto`. */ + * global (also through globalThis or an alias of it) or node:crypto's + * `webcrypto`. */ export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression): boolean { + if (stdlibGlobalNameOf(lowerer, receiver) === "crypto") return true; if (ts.isIdentifier(receiver)) { if (receiver.text === "crypto" && lowerer.isStdlibGlobal(receiver, "crypto")) return true; const imported = lowerer.builtinImportOf(receiver); diff --git a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts index 93cba84c5f..fe310294c3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts @@ -5,34 +5,57 @@ import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js"; import { lowerDynObjectLiteral } from "../expressions/object-literals.js"; import { stdlibGlobalNameOf } from "../surfaces.js"; import { WEBCRYPTO_DYN_TYPES } from "../../type-mapper.js"; +import { isWebCryptoReceiver } from "./crypto.js"; -/* WebCrypto's HMAC slice (scr_webcrypto.c): `crypto.subtle.importKey`, - * `sign`, `verify` and `digest` called on the crypto global (also through - * globalThis) lower to runtime entry points that take the argument vector - * as one dyn array and answer a promise, so Web IDL conversion, algorithm - * normalization and every error stay Node-shaped at run time. CryptoKey +/* WebCrypto's secret-key slice (scr_webcrypto.c, with encrypt and + * decrypt in scr_webcrypto_aes.c): `crypto.subtle`'s `importKey`, + * `generateKey`, `exportKey`, `encrypt`, `decrypt`, `deriveBits`, + * `deriveKey`, `sign`, `verify` and `digest` + * called on the crypto global (also through + * globalThis, node:crypto's webcrypto and node:crypto's subtle) lower to + * runtime entry points that take the argument vector as one dyn array and + * answer a promise, so Web IDL conversion, algorithm normalization and + * every error stay Node-shaped at run time. CryptoKey * values are native handles; their type maps to dyn and their members * are handle reads. Other SubtleCrypto methods are refused by name. */ const SUBTLE_LOWERED: ReadonlyMap = new Map([ ["importKey", "webcrypto.importKey"], + ["generateKey", "webcrypto.generateKey"], + ["exportKey", "webcrypto.exportKey"], + ["encrypt", "webcrypto.encrypt"], + ["decrypt", "webcrypto.decrypt"], + ["deriveBits", "webcrypto.deriveBits"], + ["deriveKey", "webcrypto.deriveKey"], ["sign", "webcrypto.sign"], ["verify", "webcrypto.verify"], ["digest", "webcrypto.digest"], ]); const SUBTLE_HINT = - "the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; + "the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; -/** Whether `node` is `crypto.subtle` on the WebCrypto global. */ +/** Whether `node` is a SubtleCrypto object: `subtle` on a WebCrypto + * Crypto object (the global, globalThis.crypto or node:crypto's + * `webcrypto`) or node:crypto's own `subtle` export, named or through + * the namespace. */ function isSubtleReceiver(lowerer: Lowerer, node: ts.Expression): boolean { while (ts.isParenthesizedExpression(node)) node = node.expression; - return ( - ts.isPropertyAccessExpression(node) && - !node.questionDotToken && - node.name.text === "subtle" && - stdlibGlobalNameOf(lowerer, node.expression) === "crypto" - ); + if (ts.isIdentifier(node)) { + const imported = lowerer.builtinImportOf(node); + return imported?.module === "crypto" && imported.member === "subtle"; + } + if ( + !ts.isPropertyAccessExpression(node) || + node.questionDotToken || + node.name.text !== "subtle" + ) { + return false; + } + if (stdlibGlobalNameOf(lowerer, node.expression) === "crypto") return true; + if (isWebCryptoReceiver(lowerer, node.expression)) return true; + const member = lowerer.builtinMemberOf(node); + return member?.module === "crypto" && member.member === "subtle"; } /** `crypto.subtle.(...args)`: the lowered methods become their @@ -51,17 +74,6 @@ export function lowerWebCryptoCall(lowerer: Lowerer, call: ts.CallExpression): I if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`crypto.subtle.${member} with spread arguments`, call, SUBTLE_HINT); } - // JsonWebKey imports are not implemented: a literal 'jwk' format is - // refused here (the runtime refuses a computed one). - const format = call.arguments.nodes[0]; - if ( - member === "importKey" && - format !== undefined && - ts.isStringLiteralLike(format) && - format.text === "jwk" - ) { - lowerer.noLowering("crypto.subtle.importKey with the 'jwk' key format", format, SUBTLE_HINT); - } const loc = locOf(call); const args = call.arguments.nodes.map((arg) => ts.isObjectLiteralExpression(arg) diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index b5f99d6826..16cca3b13a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -549,6 +549,9 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: const callable = lowerer.lowerBuiltinCallableValue(bi, loc); if (callable) return callable; } + // node:crypto's webcrypto is the WebCrypto global's object. + if (bi.module === "crypto" && bi.member === "webcrypto") + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; // JavaScript sources: an otherwise unsupported builtin VALUE is // the same identity-token story as stdlib globals above (the // harness adds worker_threads.Worker to its identity Set). @@ -721,7 +724,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: return { kind: "closure", fnName: name, captures: [], type, loc }; } if (lowerer.isStdlibGlobal(expr, "crypto")) - return { kind: "libCall", fn: "crypto.native", args: [], type: DYN, loc }; + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; const nativeAlias = stdlibGlobalNameOf(lowerer, expr); if (nativeAlias === "Buffer" && expr.text !== "Buffer") { return { kind: "strLit", value: "[builtin Buffer]", type: STRING, loc }; diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index f0b980da05..5cd70d2a5f 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -162,6 +162,7 @@ import { lowerUnionFieldRead } from "./union-fields.js"; import { errorPropertyRead } from "../error-methods.js"; import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js"; import { isZlibStreamMember, lowerZlibStreamBytesWritten } from "../builtins/compression.js"; +import { isWebCryptoReceiver } from "../builtins/crypto.js"; import { isFsStreamMember, lowerFsStreamProperty } from "../builtins/fs-streams.js"; import { keyedFieldRead } from "./keyed-field-targets.js"; import { promisePrototypeValue } from "../thenables.js"; @@ -842,8 +843,16 @@ export function lowerPropertyAccessExpr( if (runtimeOptionalClassField(lowerer, handled, expr)) return handled; return iteratorResultValueRead(lowerer, handled, expr) ?? lowerer.maybeNarrow(handled, expr); } + // `globalThis.crypto` is the WebCrypto object the bare `crypto` answers. + if ( + !expr.questionDotToken && + stdlibGlobalNameOf(lowerer, expr.expression) === "globalThis" && + stdlibGlobalNameOf(lowerer, expr) === "crypto" + ) { + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; + } // `globalThis.` that no lowering above claimed - // (globalThis.crypto, globalThis.SubtleCrypto, globalThis.localStorage + // (globalThis.SubtleCrypto, globalThis.localStorage // — the harness's capability-conditional knownGlobals adds): the // PROPERTY spelling of the identifier chokepoint's JS rule — a stdlib // global taken as a VALUE in a JavaScript source is the same opaque @@ -2959,6 +2968,9 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi if ( receiverIr?.kind !== "object" && propSym && + // The WebCrypto object's generic getRandomValues reads off its + // dynamic object. + !isWebCryptoReceiver(lowerer, expr.expression) && isGenericCallableMemberType(lowerer.checker.getTypeOfSymbol(propSym), lowerer.checker) && // CLASS members keep the class-path fences (a poisoned class's own // diagnostics, the bound-method fence above). diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 7102f37ab1..c503f0c9c8 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1542,9 +1542,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.fenceStaticReadableStreamMember(expr.expression, "call") ?? lowerDisposableStackUseCall(lowerer, expr, expr.expression) ?? lowerer.lowerStdlibMethodCall(expr, expr.expression) ?? - // crypto.getRandomValues on the WebCrypto global (untyped in - // fallback builds, so before the dyn receiver path) or webcrypto. - lowerWebCryptoGetRandomValues(lowerer, expr, expr.expression) ?? + // crypto.getRandomValues / randomUUID on the WebCrypto global + // (untyped in fallback builds, so before the dyn receiver path) or + // webcrypto. + lowerWebCryptoMethodCall(lowerer, expr, expr.expression) ?? // Dyn receivers (JSON.parse-derived `unknown`/`any` values) — lowerDynReceiverMethodCall(lowerer, expr, expr.expression) ?? @@ -10492,20 +10493,40 @@ export function lowerStaticNumberGlobal( return null; } -/** `crypto.getRandomValues(view)` on the WebCrypto global or webcrypto. */ -function lowerWebCryptoGetRandomValues( +/** `crypto.getRandomValues(view)` and `crypto.randomUUID()` on the + * WebCrypto global or webcrypto. Other argument counts (JavaScript only) + * invoke the dynamic object's method, which ignores extra arguments and + * throws Node's missing-argument TypeError. */ +function lowerWebCryptoMethodCall( lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { + const member = access.name.text; if ( - access.name.text !== "getRandomValues" || + (member !== "getRandomValues" && member !== "randomUUID") || call.questionDotToken || access.questionDotToken || !isWebCryptoReceiver(lowerer, access.expression) ) return null; - return lowerCryptoGetRandomValues(lowerer, call); + const args = call.arguments.nodes; + if (member === "getRandomValues" && args.length === 1) { + return lowerCryptoGetRandomValues(lowerer, call); + } + if (member === "randomUUID" && args.length === 0) { + return { kind: "libCall", fn: "crypto.randomUUID", args: [], type: STRING, loc: locOf(call) }; + } + if (args.some(ts.isSpreadElement)) return null; + return { + kind: "dynInvoke", + recv: lowerer.lowerExprExpecting(access.expression, DYN), + method: member, + calleeName: v8CalleeText(access), + args: args.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + type: DYN, + loc: locOf(call), + }; } /** A then handler's promise-armed result (`.then(() => cached ?? load())` diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index e3480cf9c8..7f0854523c 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -3964,9 +3964,13 @@ function stdlibGlobalTokenDestructure( const canonical = ts.symbolName(member) === "global" ? "globalThis" : ts.symbolName(member); token = `[builtin ${canonical}]`; alias = canonical; - // Builtin constructors read as their interned function values, the - // same values the bare and `globalThis.X` spellings read. - value = builtinGlobalValue(canonical, locOf(el), true); + // Builtin constructors read as their interned function values, and + // crypto as the WebCrypto object: the same values the bare and + // `globalThis.X` spellings read. + value = + canonical === "crypto" + ? { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc: locOf(el) } + : builtinGlobalValue(canonical, locOf(el), true); } else if (globalName === "console" && !CONSOLE_CALL_MEMBERS.has(propName)) { token = `[builtin console.${propName}]`; } else if (TOKEN_OPAQUE_GLOBALS.has(globalName)) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ee8ba7ecba..3fbb110ed6 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -13410,6 +13410,10 @@ export class Lowerer { const callable = this.lowerBuiltinCallableValue(bi, loc); if (callable) return callable; } + // node:crypto's webcrypto is the WebCrypto global's object. + if (bi.module === "crypto" && bi.member === "webcrypto") { + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; + } // node:stream/web's exports are the web stream globals themselves: in // JavaScript sources the same identity token as the global spelling // (`require("stream/web").ReadableStream === globalThis.ReadableStream`). diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index c779383376..67950a53f4 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2210,15 +2210,26 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ id: "stdlib.webcrypto.subtle", kind: "stdlib", name: "globalThis.crypto.subtle", - fns: ["webcrypto.importKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest"], - note: "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", + fns: [ + "webcrypto.importKey", + "webcrypto.generateKey", + "webcrypto.exportKey", + "webcrypto.encrypt", + "webcrypto.decrypt", + "webcrypto.deriveBits", + "webcrypto.deriveKey", + "webcrypto.sign", + "webcrypto.verify", + "webcrypto.digest", + ], + note: "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", }, { id: "stdlib.webcrypto.CryptoKey", kind: "stdlib", name: "CryptoKey", fns: ["webcrypto.isCryptoKey"], - note: "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey", + note: "secret HMAC, AES, PBKDF2 and HKDF keys with type, extractable, algorithm and usages, and instanceof CryptoKey", }, { id: "stdlib.atomics", @@ -3088,9 +3099,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< setFips: "a compiled binary has no FIPS provider to enable, and Node itself throws on setFips(true) " + "in a non-FIPS build — getFips() answers 0 here", - webcrypto: - "the WebCrypto object has no lowering — the lowered crypto surface is randomUUID, " + - "randomBytes, and the createHash chain", }, zlib: { brotliCompressSync: BROTLI_HINT, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index a233f0186d..a491edf958 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -146,6 +146,16 @@ export const WEBCRYPTO_DYN_TYPES: ReadonlySet = new Set([ "KeyAlgorithm", "HmacImportParams", "HmacKeyAlgorithm", + "HmacKeyGenParams", + "AesKeyGenParams", + "AesKeyAlgorithm", + "JsonWebKey", + "AesGcmParams", + "AesCbcParams", + "AesCtrParams", + "Pbkdf2Params", + "HkdfParams", + "AesDerivedKeyParams", ]); export const FETCH_AMBIENT_TYPES = [ diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fa78765791..416d5f18e1 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2846,10 +2846,17 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * one dyn array and answers a promise that argument and validation * failures reject. CryptoKey values are native handles. */ "webcrypto.importKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.generateKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.exportKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.encrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.decrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.deriveBits": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.deriveKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.sign": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.verify": { argTypes: [DYN], result: { kind: "promise", inner: BOOL } }, "webcrypto.digest": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.isCryptoKey": { argTypes: [DYN], result: BOOL }, + "webcrypto.object": { argTypes: [], result: DYN }, "crypto.randomUUIDv7": { argTypes: [DYN], result: STRING }, "crypto.pbkdf2": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: BYTES_U8 }, "crypto.randomBytesCb": { argTypes: [F64, null], result: VOID }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 4142b6efe3..61e04177b6 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -75,6 +75,8 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "child.propsInstall", "child.propsResolve", "crypto.native", + // The crypto global's object extends crypto.native's. + "webcrypto.object", "dyn.promiseAll", "dyn.promiseAllSettled", "dyn.promiseAny", @@ -647,8 +649,9 @@ const RUNTIME_FEATURE_RULES = { }), /** scr_cipher.c over the mbedTLS archive; DEP0182 reports through the * process-warning unit, and unknown output encodings render through the - * inspect unit. */ - cipher: rule({ fns: ["crypto.cipherNew"] }), + * inspect unit. WebCrypto's encrypt and decrypt (scr_webcrypto_aes.c) + * link the same archive. */ + cipher: rule({ fns: ["crypto.cipherNew", "webcrypto.encrypt", "webcrypto.decrypt"] }), copying: rule({ expr: (node, _tables) => (node.kind === "arrIntrinsic" && COPYING_ARR_METHODS.has(node.method)) || @@ -675,12 +678,15 @@ const RUNTIME_FEATURE_RULES = { ), // The legacy url functions, crypto.createCipheriv and // ecdh.setPublicKey emit their deprecation warnings through the - // dyn-async unit; scr_wasm.c boxes its handles and promises through it. + // dyn-async unit (WebCrypto's encrypt and decrypt link scr_cipher.c + // too); scr_wasm.c boxes its handles and promises through it. fns: [ ...DYN_ASYNC_LIB_FNS, ...READABLE_DYN_ASYNC_LIB_FNS, ...LEGACY_URL_LIB_FNS, "crypto.cipherNew", + "webcrypto.encrypt", + "webcrypto.decrypt", "crypto.ecdhNew", ], prefixes: [...READABLE_DYN_ASYNC_PREFIXES, "wasm."], @@ -787,6 +793,8 @@ const RUNTIME_FEATURE_RULES = { "global.native", "util.styleText", "crypto.cipherNew", + "webcrypto.encrypt", + "webcrypto.decrypt", "crypto.ecdhNew", "crypto.ecdhConvertKey", ...BUILTIN_CLASS_LIB_FNS, diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index c05e994a8f..3e8b9df731 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -762,14 +762,6 @@ "code": "SC2020", "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" }, - { - "id": "node-builtin.crypto.webcrypto", - "kind": "node-builtin", - "name": "crypto.webcrypto", - "status": "unsupported", - "code": "SC2020", - "note": "the WebCrypto object has no lowering — the lowered crypto surface is randomUUID, randomBytes, and the createHash chain" - }, { "id": "node-builtin.dgram", "kind": "node-builtin", @@ -5719,14 +5711,14 @@ "kind": "stdlib", "name": "CryptoKey", "status": "static", - "note": "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey" + "note": "secret HMAC, AES, PBKDF2 and HKDF keys with type, extractable, algorithm and usages, and instanceof CryptoKey" }, { "id": "stdlib.webcrypto.subtle", "kind": "stdlib", "name": "globalThis.crypto.subtle", "status": "static", - "note": "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" + "note": "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" }, { "id": "syntax.async-generators", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7120-webcrypto-random-uuid.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7120-webcrypto-random-uuid.ts.json new file mode 100644 index 0000000000..c70fdfa635 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7120-webcrypto-random-uuid.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7120-webcrypto-random-uuid.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7121-webcrypto-crypto-object.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7121-webcrypto-crypto-object.mjs.json new file mode 100644 index 0000000000..b152699bae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7121-webcrypto-crypto-object.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7121-webcrypto-crypto-object.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-webcrypto-node-crypto.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-webcrypto-node-crypto.ts.json new file mode 100644 index 0000000000..02451a516a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-webcrypto-node-crypto.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7122-webcrypto-node-crypto.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-webcrypto-node-crypto.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-webcrypto-node-crypto.cjs.json new file mode 100644 index 0000000000..821fb378dc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-webcrypto-node-crypto.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7123-webcrypto-node-crypto.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-webcrypto-secret-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-webcrypto-secret-keys.ts.json new file mode 100644 index 0000000000..8fc0dab2a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-webcrypto-secret-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7124-webcrypto-secret-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-webcrypto-key-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-webcrypto-key-errors.mjs.json new file mode 100644 index 0000000000..942de8c101 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-webcrypto-key-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7125-webcrypto-key-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-webcrypto-aes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-webcrypto-aes.ts.json new file mode 100644 index 0000000000..f113a3ca9a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-webcrypto-aes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7126-webcrypto-aes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-webcrypto-aes-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-webcrypto-aes-errors.mjs.json new file mode 100644 index 0000000000..3ec3d26f22 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-webcrypto-aes-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7127-webcrypto-aes-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-webcrypto-derive.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-webcrypto-derive.ts.json new file mode 100644 index 0000000000..b613fbce64 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-webcrypto-derive.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7128-webcrypto-derive.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7129-webcrypto-derive-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7129-webcrypto-derive-errors.mjs.json new file mode 100644 index 0000000000..11fc8c7215 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7129-webcrypto-derive-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7129-webcrypto-derive-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-keys.ts.json new file mode 100644 index 0000000000..19f14ed02a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/webcrypto-keys.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 85c5ffb000..3944b023c4 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -172,7 +172,8 @@ async function build() { ...(unit.source === "scr_regex.c" ? ["-I", quickjs] : []), ...(unit.source === "scr_tls.c" || unit.source === "scr_cipher.c" || - unit.source === "scr_ecdh.c" + unit.source === "scr_ecdh.c" || + unit.source === "scr_webcrypto_aes.c" ? ["-I", join(mbedtls, "include")] : []), ...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c" diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 0f40218a18..3529df137f 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -72,6 +72,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_js_object.c", "dynTier"], ["scr_event_target.c", any("eventTarget", "nativeFetch")], ["scr_async_dyn.c", any("dynAsync", "dynInvoke", "dc", "fileHandle", "nativeFetch")], + // The crypto global's object extends scr_async_dyn.c's crypto object. + ["scr_webcrypto_object.c", all("webcrypto", "dynAsync")], // The performance timeline emits its buffer warning through scr_async_dyn.c. ["scr_perf.c", "dynAsync"], // perf_hooks event-loop metrics, only for programs that read them. @@ -151,6 +153,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_ecdh.c", "ecdh"], ["scr_wasm.c", "wasm"], ["scr_webcrypto.c", "webcrypto"], + // WebCrypto's AES ciphers run over the cipher feature's mbedTLS archive. + ["scr_webcrypto_aes.c", all("cipher", "webcrypto")], ["scr_fetch.c", "nativeFetch"], ]; diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index b242b78aa5..f59a2431fe 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -223,7 +223,8 @@ async function build() { ...(unit.source === "scr_regex.c" ? ["-I", quickjs] : []), ...(unit.source === "scr_tls.c" || unit.source === "scr_cipher.c" || - unit.source === "scr_ecdh.c" + unit.source === "scr_ecdh.c" || + unit.source === "scr_webcrypto_aes.c" ? ["-I", join(mbedtls, "include")] : []), ...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c" diff --git a/packages/runtime-wasm32-wasi/scripts/build.mjs b/packages/runtime-wasm32-wasi/scripts/build.mjs index 2dd9839731..0a8699b23b 100644 --- a/packages/runtime-wasm32-wasi/scripts/build.mjs +++ b/packages/runtime-wasm32-wasi/scripts/build.mjs @@ -22,6 +22,7 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ "scr_tls.c", "scr_cipher.c", "scr_ecdh.c", + "scr_webcrypto_aes.c", "scr_wasm.c", "scr_fetch.c", ], diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index d12858abfa..ddc306598b 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -20,6 +20,9 @@ ScrStr *scr_crypto_hash_digest_bytes(ScrStr *alg, ScrBytes *data, ScrStr *enc); typedef struct ScrCryptoHash ScrCryptoHash; ScrCryptoHash *scr_crypto_hash_new(ScrStr *alg); ScrDyn *scr_crypto_native(void); +/* The crypto global as a value: scr_crypto_native's object with the Crypto + * methods (scr_webcrypto_object.c, linked with the webcrypto feature). */ +ScrDyn *scr_webcrypto_object(void); ScrCryptoHash *scr_crypto_hmac_new_str(ScrStr *alg, ScrStr *key); ScrCryptoHash *scr_crypto_hmac_new_bytes(ScrStr *alg, ScrBytes *key); ScrCryptoHash *scr_crypto_hash_retain(ScrCryptoHash *h); @@ -202,19 +205,48 @@ size_t scr_crypto_digest_raw(const char *alg, const unsigned char *data, size_t size_t scr_crypto_hmac_raw(const char *alg, const unsigned char *key, size_t keylen, const unsigned char *data, size_t len, unsigned char out[64]); /* ── WebCrypto (scr_webcrypto.c) ───────────────────────────────────── - * crypto.subtle.importKey (raw HMAC keys), sign and verify (HMAC) and - * digest (SHA-1/SHA-2) over node:crypto's digest core. Each takes the + * crypto.subtle.importKey (HMAC, AES, PBKDF2 and HKDF keys, raw and jwk), + * generateKey and exportKey (HMAC and AES keys), sign and verify (HMAC) + * and digest (SHA-1/SHA-2) over node:crypto's digest core. Each takes the * call's argument vector as one dyn array (borrowed) and answers a +1 * promise that every argument or validation failure rejects with Node's * TypeError or DOMException; algorithms and formats outside the slice - * reject with a coded SC2020 Error. importKey settles at once with a - * CryptoKey handle; sign and digest fulfill with an ArrayBuffer and - * verify with a boolean on a later loop turn. scr_webcrypto_is_crypto_key - * answers `value instanceof CryptoKey`. */ + * reject with a coded SC2020 Error. importKey and exportKey settle at + * once with a CryptoKey handle, an ArrayBuffer or a JWK object; + * generateKey fulfills with a CryptoKey, sign and digest with an + * ArrayBuffer and verify with a boolean on a later loop turn. + * scr_webcrypto_is_crypto_key answers `value instanceof CryptoKey`. */ ScrPromise *scr_webcrypto_import_key(ScrDyn *args); +ScrPromise *scr_webcrypto_generate_key(ScrDyn *args); +ScrPromise *scr_webcrypto_export_key(ScrDyn *args); ScrPromise *scr_webcrypto_sign(ScrDyn *args); ScrPromise *scr_webcrypto_verify(ScrDyn *args); ScrPromise *scr_webcrypto_digest(ScrDyn *args); +/* encrypt and decrypt (scr_webcrypto_aes.c, linked with the cipher + * feature's mbedTLS archive) run scr_webcrypto_cipher with the AES cipher + * as `aes`, which answers the +1 output bytes or NULL when the operation + * fails. encrypt and decrypt fulfill with an ArrayBuffer, or reject with + * a job failure, on a later loop turn. */ +typedef struct { + const unsigned char *iv; /* the IV, or the CTR counter block */ + size_t iv_len; + const unsigned char *aad; + size_t aad_len; + size_t tag_len; /* bytes */ + unsigned counter_bits; +} ScrWebCryptoAesParams; +typedef ScrBytes *(*ScrWebCryptoAesFn)(const char *name, bool decrypt, const unsigned char *key, + size_t key_len, const ScrWebCryptoAesParams *params, + const unsigned char *in, size_t len); +ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn aes); +ScrPromise *scr_webcrypto_encrypt(ScrDyn *args); +ScrPromise *scr_webcrypto_decrypt(ScrDyn *args); +/* deriveBits and deriveKey (PBKDF2 and HKDF) over node:crypto's KDF core: + * deriveBits fulfills with an ArrayBuffer (at once for a zero length) and + * deriveKey with a CryptoKey of the derived key type, on a later loop + * turn. */ +ScrPromise *scr_webcrypto_derive_bits(ScrDyn *args); +ScrPromise *scr_webcrypto_derive_key(ScrDyn *args); bool scr_webcrypto_is_crypto_key(const ScrDyn *value); /* The composed `new crypto.X509Certificate(data).fingerprint` read (the diff --git a/packages/runtime/src/scr_webcrypto.c b/packages/runtime/src/scr_webcrypto.c index 546b241518..4d2c27be42 100644 --- a/packages/runtime/src/scr_webcrypto.c +++ b/packages/runtime/src/scr_webcrypto.c @@ -1,22 +1,27 @@ -/* WebCrypto's HMAC slice: crypto.subtle.importKey (raw HMAC keys), - * sign, verify and the SHA-1/SHA-2 digests, plus the CryptoKey values - * importKey answers. +/* WebCrypto's secret-key slice: crypto.subtle.importKey, generateKey and + * exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF + * keys), HMAC sign and verify, the SHA-1/SHA-2 digests, PBKDF2 and HKDF + * deriveBits and deriveKey, the argument and key handling of AES encrypt + * and decrypt (whose cipher runs in scr_webcrypto_aes.c), and the + * CryptoKey values those methods answer. * * This unit is only the WebCrypto layer, modeled on Node 24's - * lib/internal/crypto/webcrypto.js and mac.js: Web IDL argument - * conversion, algorithm normalization, the CryptoKey handle, promise - * wrapping and Node's error shapes. The MACs and digests themselves come - * from node:crypto's digest core (scr_crypto_hmac_raw and - * scr_crypto_digest_raw in scr_lib.c), and results settle through the - * deferred crypto queue that node:crypto's callback forms use, so sign, - * verify and digest settle on a later loop turn like Node's thread-pool - * jobs while importKey settles at once. + * lib/internal/crypto/webcrypto.js, mac.js, aes.js and keys.js: Web IDL + * argument conversion, algorithm normalization, the CryptoKey handle, the + * JSON Web Key forms, promise wrapping and Node's error shapes. The MACs + * and digests themselves come from node:crypto's digest core + * (scr_crypto_hmac_raw and scr_crypto_digest_raw in scr_lib.c), and + * results settle through the deferred crypto queue that node:crypto's + * callback forms use, so sign, verify, digest and generateKey settle on a + * later loop turn like Node's thread-pool jobs while importKey and + * exportKey settle at once. * * Every argument and validation failure rejects the returned promise. * Algorithms Node recognizes but this slice does not implement (RSA, - * ECDSA, AES, SHA-3, ...) and the 'jwk' key format reject with a coded - * SC2020 Error instead of silently diverging. The compiler links the unit - * only into programs that call these methods (the webcrypto feature). */ + * ECDSA, AES-OCB, SHA-3, ...) and the key formats of asymmetric keys + * reject with a coded SC2020 Error instead of silently diverging. The + * compiler links the unit only into programs that call these methods + * (the webcrypto feature). */ #include "scr_runtime.h" #include @@ -28,8 +33,9 @@ /* ── algorithm registry ─────────────────────────────────────────────── * The names Node 24 registers per operation (util.js's * kAlgorithmDefinitions with the conditional algorithms its OpenSSL - * build enables), matched case-insensitively. Only HMAC and the SHA-1/ - * SHA-2 digests are implemented. */ + * build enables), matched case-insensitively. HMAC, AES-CBC/CTR/GCM/KW + * keys, PBKDF2 and HKDF key imports and the SHA-1/SHA-2 digests are + * implemented. */ static const char *const scr_wc_import_names[] = { "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", "Argon2d", "Argon2i", "Argon2id", "ChaCha20-Poly1305", @@ -43,6 +49,15 @@ static const char *const scr_wc_sign_names[] = { "ECDSA", "Ed25519", "Ed448", "HMAC", "KMAC128", "KMAC256", "ML-DSA-44", "ML-DSA-65", "ML-DSA-87", "RSA-PSS", "RSASSA-PKCS1-v1_5", }; +/* generateKey and exportKey register the same algorithms. */ +static const char *const scr_wc_key_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", + "ChaCha20-Poly1305", "ECDH", "ECDSA", "Ed25519", "Ed448", + "HMAC", "KMAC128", "KMAC256", "ML-DSA-44", "ML-DSA-65", + "ML-DSA-87", "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024", "MLKEM768-P256", + "MLKEM768-X25519", "MLKEM1024-P384", "RSA-OAEP", "RSA-PSS", "RSASSA-PKCS1-v1_5", + "X25519", "X448", +}; static const char *const scr_wc_digest_names[] = { "SHA-1", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", "SHA3-512", "cSHAKE128", "cSHAKE256", "KT128", "KT256", "TurboSHAKE128", @@ -193,7 +208,24 @@ static const char *const scr_wc_usages[] = { "deriveKey", "deriveBits", "wrapKey", "unwrapKey", "encapsulateKey", "encapsulateBits", "decapsulateKey", "decapsulateBits", }; -enum { SCR_WC_USAGE_SIGN = 1u << 2, SCR_WC_USAGE_VERIFY = 1u << 3 }; +enum { + SCR_WC_USAGE_ENCRYPT = 1u << 0, + SCR_WC_USAGE_DECRYPT = 1u << 1, + SCR_WC_USAGE_SIGN = 1u << 2, + SCR_WC_USAGE_VERIFY = 1u << 3, + SCR_WC_USAGE_DERIVE_KEY = 1u << 4, + SCR_WC_USAGE_DERIVE_BITS = 1u << 5, + SCR_WC_USAGE_WRAP_KEY = 1u << 6, + SCR_WC_USAGE_UNWRAP_KEY = 1u << 7, +}; + +/* The usage index of a KeyUsage string, or -1. */ +static int scr_wc_usage_index(const ScrStr *s) { + for (size_t k = 0; k < SCR_WC_COUNT(scr_wc_usages); k++) + if (s->len == strlen(scr_wc_usages[k]) && memcmp(s->data, scr_wc_usages[k], s->len) == 0) + return (int)k; + return -1; +} /* sequence over an array: the usage mask (-1 on failure). */ static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { @@ -215,11 +247,8 @@ static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { scr_dyn_release(plain); return -1; } - size_t k = 0; - while (k < SCR_WC_COUNT(scr_wc_usages) && - !(s->len == strlen(scr_wc_usages[k]) && memcmp(s->data, scr_wc_usages[k], s->len) == 0)) - k++; - if (k == SCR_WC_COUNT(scr_wc_usages)) { + int k = scr_wc_usage_index(s); + if (k < 0) { scr_wc_type_error("ERR_INVALID_ARG_VALUE", "%s: %s argument[%.0f] '%.*s' is not a valid enum value of type KeyUsage.", prefix, scr_wc_ordinals[index], i, (int)(s->len > 200 ? 200 : s->len), @@ -237,10 +266,10 @@ static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { /* ── algorithm normalization ────────────────────────────────────────── * normalizeAlgorithm(algorithm, op) for the members this slice reads: - * the canonical name, and for HMAC imports the hash and length. */ + * the canonical name, the HMAC hash and length, and AES key lengths. */ typedef struct { const char *name; /* canonical registered name */ - const char *hash; /* HMAC import: the normalized hash name */ + const char *hash; /* HMAC: the normalized hash name */ bool has_length; double length; } ScrWcAlgorithm; @@ -261,6 +290,28 @@ static ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key) { return v ? v : scr_dyn_retain(scr_dyn_undefined()); } +/* An [EnforceRange] integer conversion: the truncated value in [0, max], + * or false with Node's TypeError pending. `context` names the value + * ("length in passed algorithm", "3rd argument"). */ +static bool scr_wc_enforce_range(const char *prefix, const char *context, const ScrDyn *value, + double max, double *out) { + double x; + if (!scr_dyn_number_coerce_js(value, &x)) return false; + if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: %s is not a finite number.", prefix, context); + return false; + } + x = trunc(x); + if (x == 0) x = 0; + if (x < 0 || x > max) { + scr_wc_type_error("ERR_OUT_OF_RANGE", "%s: %s is outside the expected range of 0 to %.0f.", + prefix, context, max); + return false; + } + *out = x; + return true; +} + /* The name an AlgorithmIdentifier carries (+1), with the Algorithm * dictionary's missing-name TypeError. `dict` names the dictionary the * value converts to. */ @@ -295,11 +346,13 @@ static const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *metho return canonical; } -/* HmacImportParams over an object: hash (required, a SHA name), then the - * optional [EnforceRange] unsigned long length. */ -static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { +/* HmacImportParams / HmacKeyGenParams (`dict`) over an object: hash + * (required, a SHA name), then the optional [EnforceRange] unsigned long + * length, which cannot be 0 (`zero_error` names that DOMException). */ +static bool scr_wc_hmac_params(ScrDyn *identifier, const char *dict, const char *zero_error, + const char *method, ScrWcAlgorithm *out) { if (!scr_wc_is_object(identifier)) { - scr_wc_missing_member("HmacImportParams", "hash"); + scr_wc_missing_member(dict, "hash"); return false; } ScrDyn *plain = scr_wc_plain(identifier); @@ -307,7 +360,7 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { ScrDyn *hash = scr_wc_member(plain, "hash"); bool ok = true; if (hash->kind == SCR_DYN_UNDEF) { - scr_wc_missing_member("HmacImportParams", "hash"); + scr_wc_missing_member(dict, "hash"); ok = false; } ScrDyn *hash_value = NULL; @@ -335,7 +388,9 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { (hash_name->data[2] | 32) == 'a'; if (hash_name) scr_str_release(hash_name); if (ok && !sha) { - scr_throw_domex("NotSupportedError", "Only SHA hashes are supported in HmacImportParams"); + char message[96]; + snprintf(message, sizeof message, "Only SHA hashes are supported in %s", dict); + scr_throw_domex("NotSupportedError", message); ok = false; } } @@ -343,34 +398,23 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { ScrDyn *length = scr_wc_member(plain, "length"); if (length->kind != SCR_DYN_UNDEF) { double x; - if (!scr_dyn_number_coerce_js(length, &x)) ok = false; - else if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) { - scr_wc_type_error("ERR_INVALID_ARG_TYPE", - "%s: length in passed algorithm is not a finite number.", - scr_wc_normalize_prefix); + if (!scr_wc_enforce_range(scr_wc_normalize_prefix, "length in passed algorithm", length, + 4294967295.0, &x)) + ok = false; + else if (x == 0) { + char message[96]; + snprintf(message, sizeof message, "%s.length cannot be 0", dict); + scr_throw_domex(zero_error, message); ok = false; } else { - x = trunc(x); - if (x == 0) x = 0; - if (x < 0 || x > 4294967295.0) { - scr_wc_type_error("ERR_OUT_OF_RANGE", - "%s: length in passed algorithm is outside the expected range of 0 to " - "4294967295.", - scr_wc_normalize_prefix); - ok = false; - } else if (x == 0) { - scr_throw_domex("DataError", "HmacImportParams.length cannot be 0"); - ok = false; - } else { - out->has_length = true; - out->length = x; - } + out->has_length = true; + out->length = x; } } scr_dyn_release(length); } if (ok) { - out->hash = scr_wc_digest_algorithm(hash_value, "importKey"); + out->hash = scr_wc_digest_algorithm(hash_value, method); if (!out->hash) ok = false; } if (hash_value) scr_dyn_release(hash_value); @@ -379,11 +423,51 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { return ok; } -/* normalizeAlgorithm for importKey, sign/verify (`names`) — false with - * the error pending. Only HMAC parameters are read; any other recognized - * algorithm is refused by the caller. */ -static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_t count, - bool import, ScrWcAlgorithm *out) { +/* AesKeyGenParams / AesDerivedKeyParams (`dict`): the required + * [EnforceRange] unsigned short length, 128, 192 or 256. */ +static bool scr_wc_aes_length_params(ScrDyn *identifier, const char *dict, ScrWcAlgorithm *out) { + ScrDyn *plain = scr_wc_plain(identifier); + if (!plain) return false; + ScrDyn *length = scr_wc_member(plain, "length"); + bool ok = false; + double x; + if (length->kind == SCR_DYN_UNDEF) scr_wc_missing_member(dict, "length"); + else if (scr_wc_enforce_range(scr_wc_normalize_prefix, "length in passed algorithm", length, + 65535.0, &x)) { + if (x != 128 && x != 192 && x != 256) { + scr_throw_domex("OperationError", "AES key length must be 128, 192, or 256 bits"); + } else { + out->has_length = true; + out->length = x; + ok = true; + } + } + scr_dyn_release(length); + scr_dyn_release(plain); + return ok; +} + +static bool scr_wc_is_aes(const char *name) { + return strcmp(name, "AES-CBC") == 0 || strcmp(name, "AES-CTR") == 0 || + strcmp(name, "AES-GCM") == 0 || strcmp(name, "AES-KW") == 0; +} + +static bool scr_wc_is_kdf(const char *name) { + return strcmp(name, "PBKDF2") == 0 || strcmp(name, "HKDF") == 0; +} + +typedef enum { + SCR_WC_OP_IMPORT, + SCR_WC_OP_SIGN, + SCR_WC_OP_GENERATE, +} ScrWcOp; + +/* normalizeAlgorithm for importKey, sign/verify and generateKey (`names`) + * — false with the error pending. Only the dictionaries of the + * implemented algorithms are read; any other recognized algorithm is + * refused by the caller. */ +static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_t count, ScrWcOp op, + ScrWcAlgorithm *out) { memset(out, 0, sizeof *out); ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); if (!name) return false; @@ -393,19 +477,27 @@ static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_ scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); return false; } - if (import && strcmp(out->name, "HMAC") == 0) return scr_wc_hmac_import_params(identifier, out); + bool hmac = strcmp(out->name, "HMAC") == 0; + if (op == SCR_WC_OP_IMPORT && hmac) + return scr_wc_hmac_params(identifier, "HmacImportParams", "DataError", "importKey", out); + if (op == SCR_WC_OP_GENERATE && hmac) + return scr_wc_hmac_params(identifier, "HmacKeyGenParams", "OperationError", "generateKey", out); + if (op == SCR_WC_OP_GENERATE && scr_wc_is_aes(out->name)) + return scr_wc_aes_length_params(identifier, "AesKeyGenParams", out); return true; } /* ── CryptoKey ───────────────────────────────────────────────────────── - * A secret HMAC key: the bytes, the hash, the bit length, the usage mask - * and the extractable flag. The algorithm and usages getters answer the - * same object on every read (Node caches its clones), built on first - * read. The bytes are cleared when the last reference goes. */ + * A secret key: the algorithm name, the bytes, the HMAC hash, the bit + * length (HMAC and AES keys), the usage mask and the extractable flag. + * The algorithm and usages getters answer the same object on every read + * (Node caches its clones), built on first read. The bytes are cleared + * when the last reference goes. */ typedef struct ScrCryptoKey { size_t rc; - const char *hash; /* WebCrypto hash name */ - double length; /* bits */ + const char *name; /* canonical algorithm name */ + const char *hash; /* HMAC: WebCrypto hash name */ + double length; /* HMAC and AES: bits */ unsigned usages; bool extractable; ScrDyn *algorithm; /* cached getter results, or NULL */ @@ -446,26 +538,34 @@ static ScrDyn *scr_wc_cstr(const char *s) { return out; } +/* Node's algorithm objects: { name, length, hash } for HMAC keys, + * { name, length } for AES keys and { name } for PBKDF2 and HKDF keys. */ static ScrDyn *scr_crypto_key_algorithm(ScrCryptoKey *key) { if (!key->algorithm) { - ScrDyn *hash = scr_dyn_new_obj(); - scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(key->hash)); ScrDyn *algorithm = scr_dyn_new_obj(); - scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr("HMAC")); - scr_dyn_obj_set(algorithm, "length", 6, scr_dyn_new_num(key->length)); - scr_dyn_obj_set(algorithm, "hash", 4, hash); + scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr(key->name)); + if (!scr_wc_is_kdf(key->name)) + scr_dyn_obj_set(algorithm, "length", 6, scr_dyn_new_num(key->length)); + if (key->hash) { + ScrDyn *hash = scr_dyn_new_obj(); + scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(key->hash)); + scr_dyn_obj_set(algorithm, "hash", 4, hash); + } key->algorithm = algorithm; } return scr_dyn_retain(key->algorithm); } +/* The usages as a fresh array in Node's canonical order (+1). */ +static ScrDyn *scr_wc_usage_array(unsigned usages) { + ScrDyn *list = scr_dyn_new_arr(); + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) + if (usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i])); + return list; +} + static ScrDyn *scr_crypto_key_usages(ScrCryptoKey *key) { - if (!key->usage_list) { - ScrDyn *list = scr_dyn_new_arr(); - for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) - if (key->usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i])); - key->usage_list = list; - } + if (!key->usage_list) key->usage_list = scr_wc_usage_array(key->usages); return scr_dyn_retain(key->usage_list); } @@ -540,8 +640,9 @@ static const ScrDynHandleOps scr_crypto_key_ops = { .symbol_get = scr_crypto_key_symbol_get, }; -static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const char *hash, - double length, unsigned usages, bool extractable) { +static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const char *name, + const char *hash, double length, unsigned usages, + bool extractable) { static SCR_TL bool installed; if (!installed) { installed = true; @@ -550,6 +651,7 @@ static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const c ScrCryptoKey *key = malloc(sizeof *key + (len ? len : 1)); if (!key) scr_trap("scriptc: out of memory\n"); key->rc = 1; + key->name = name; key->hash = hash; key->length = length; key->usages = usages; @@ -563,6 +665,208 @@ static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const c return box; } +/* numBitsToBytes: whole bytes for `bits`, rounding up. */ +static size_t scr_wc_bits_to_bytes(double bits) { + return (size_t)(floor(bits / 8) + floor((7 + fmod(bits, 8)) / 8)); +} + +/* Clears the unused low bits of the final byte of a `bits`-long key + * (truncateToBitLength). */ +static void scr_wc_truncate_bits(unsigned char *data, size_t len, double bits) { + int extra = (int)fmod(bits, 8); + if (extra && len) data[len - 1] = (unsigned char)(data[len - 1] & (0xff << (8 - extra))); +} + +/* The usages each key algorithm accepts. */ +static unsigned scr_wc_allowed_usages(const char *name) { + if (strcmp(name, "HMAC") == 0) return SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY; + if (strcmp(name, "AES-KW") == 0) return SCR_WC_USAGE_WRAP_KEY | SCR_WC_USAGE_UNWRAP_KEY; + if (scr_wc_is_kdf(name)) return SCR_WC_USAGE_DERIVE_KEY | SCR_WC_USAGE_DERIVE_BITS; + return SCR_WC_USAGE_ENCRYPT | SCR_WC_USAGE_DECRYPT | SCR_WC_USAGE_WRAP_KEY | + SCR_WC_USAGE_UNWRAP_KEY; +} + +/* validateKeyUsages: SyntaxError for a usage outside the algorithm's. */ +static bool scr_wc_check_usages(const char *name, long usages) { + if (!(usages & ~(long)scr_wc_allowed_usages(name))) return true; + char message[96]; + snprintf(message, sizeof message, "Unsupported key usage for %s%s key", + scr_wc_is_kdf(name) ? "a " : "", name); + scr_throw_domex("SyntaxError", message); + return false; +} + +/* The JWK "alg" of an HMAC hash or an AES key, or NULL. */ +static const char *scr_wc_jwk_alg(const char *name, const char *hash, double length, char *buf, + size_t size) { + if (strcmp(name, "HMAC") == 0) { + if (strcmp(hash, "SHA-1") == 0) return "HS1"; + if (strcmp(hash, "SHA-256") == 0) return "HS256"; + if (strcmp(hash, "SHA-384") == 0) return "HS384"; + if (strcmp(hash, "SHA-512") == 0) return "HS512"; + return NULL; + } + if (!scr_wc_is_aes(name)) return NULL; + snprintf(buf, size, "A%.0f%s", length, name + 4); + return buf; +} + +/* ── JSON Web Keys ───────────────────────────────────────────────────── + * The JsonWebKey dictionary members a secret key reads, converted in Web + * IDL's member order (every DOMString member converts, so a Symbol or a + * throwing toString in any of them rejects like Node). */ +typedef struct { + ScrStr *alg, *k, *kty, *use; + bool has_ext, ext; + ScrDyn *key_ops; /* array of strings, or NULL */ +} ScrWcJwk; + +static void scr_wc_jwk_drop(ScrWcJwk *jwk) { + if (jwk->alg) scr_str_release(jwk->alg); + if (jwk->k) scr_str_release(jwk->k); + if (jwk->kty) scr_str_release(jwk->kty); + if (jwk->use) scr_str_release(jwk->use); + if (jwk->key_ops) scr_dyn_release(jwk->key_ops); + memset(jwk, 0, sizeof *jwk); +} + +/* A DOMString dictionary member: NULL with *ok when absent; NULL with + * !*ok and the TypeError pending on failure. */ +static ScrStr *scr_wc_jwk_string(const char *prefix, const ScrDyn *dict, const char *key, bool *ok) { + ScrDyn *value = scr_wc_member(dict, key); + ScrStr *out = NULL; + if (value->kind == SCR_DYN_SYMBOL) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: %s in 2nd argument is a Symbol and cannot be converted to a string.", + prefix, key); + *ok = false; + } else if (value->kind != SCR_DYN_UNDEF) { + out = scr_wc_dom_string(value); + if (!out) *ok = false; + } + scr_dyn_release(value); + return out; +} + +static bool scr_wc_jwk_convert(const char *prefix, ScrDyn *value, ScrWcJwk *out) { + memset(out, 0, sizeof *out); + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) return true; + if (!scr_wc_is_object(value)) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument cannot be converted to a dictionary", + prefix); + return false; + } + ScrDyn *dict = scr_wc_plain(value); + if (!dict) return false; + static const char *const members[] = {"alg", "crv", "d", "dp", "dq", "e", "ext", + "k", "key_ops", "kty", "n", "p", "priv", "pub", + "q", "qi", "use", "x", "y"}; + bool ok = true; + for (size_t i = 0; ok && i < SCR_WC_COUNT(members); i++) { + const char *key = members[i]; + if (strcmp(key, "ext") == 0) { + ScrDyn *ext = scr_wc_member(dict, key); + if (ext->kind != SCR_DYN_UNDEF) { + out->has_ext = true; + out->ext = scr_dyn_truthy(ext); + } + scr_dyn_release(ext); + } else if (strcmp(key, "key_ops") == 0) { + ScrDyn *ops = scr_wc_member(dict, key); + if (ops->kind != SCR_DYN_UNDEF) { + ScrDyn *list = scr_wc_plain(ops); + if (!list) ok = false; + else if (list->kind != SCR_DYN_ARR) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: key_ops in 2nd argument cannot be converted to sequence.", prefix); + scr_dyn_release(list); + ok = false; + } else { + ScrDyn *strings = scr_dyn_new_arr(); + double n = scr_dyn_arr_len(list); + for (double j = 0; ok && j < n; j++) { + ScrDyn *item = scr_dyn_arr_at(list, j); + ScrStr *s = scr_wc_dom_string(item); + scr_dyn_release(item); + if (!s) ok = false; + else { + scr_dyn_arr_push(strings, scr_dyn_new_str(s)); + scr_str_release(s); + } + } + scr_dyn_release(list); + if (ok) out->key_ops = strings; + else scr_dyn_release(strings); + } + } + scr_dyn_release(ops); + } else { + ScrStr *s = scr_wc_jwk_string(prefix, dict, key, &ok); + if (strcmp(key, "alg") == 0) out->alg = s; + else if (strcmp(key, "k") == 0) out->k = s; + else if (strcmp(key, "kty") == 0) out->kty = s; + else if (strcmp(key, "use") == 0) out->use = s; + else if (s) scr_str_release(s); + } + } + scr_dyn_release(dict); + if (!ok) scr_wc_jwk_drop(out); + return ok; +} + +static bool scr_wc_str_is(const ScrStr *s, const char *literal) { + return s && s->len == strlen(literal) && memcmp(s->data, literal, s->len) == 0; +} + +/* validateJwk for an oct key, then the key bytes (+1): k decodes with + * Node's lenient base64 decoder, so any string answers bytes. */ +static ScrBytes *scr_wc_jwk_secret(const ScrWcJwk *jwk, bool extractable, long usages, + const char *expected_use) { + if (!jwk->kty) { + scr_throw_domex("DataError", "Invalid keyData"); + return NULL; + } + if (!scr_wc_str_is(jwk->kty, "oct")) { + scr_throw_domex("DataError", "Invalid JWK \"kty\" Parameter"); + return NULL; + } + if (!jwk->k) { + scr_throw_domex("DataError", "Invalid keyData"); + return NULL; + } + if (usages != 0 && jwk->use && !scr_wc_str_is(jwk->use, expected_use)) { + scr_throw_domex("DataError", "Invalid JWK \"use\" Parameter"); + return NULL; + } + if (jwk->key_ops) { + long seen = 0; + double n = scr_dyn_arr_len(jwk->key_ops); + for (double i = 0; i < n; i++) { + ScrDyn *op = scr_dyn_arr_at(jwk->key_ops, i); + int k = scr_wc_usage_index(op->v.str); + scr_dyn_release(op); + if (k < 0) continue; + if (seen & (1L << k)) { + scr_throw_domex("DataError", "Duplicate key operation"); + return NULL; + } + seen |= 1L << k; + } + if ((seen & usages) != usages) { + scr_throw_domex("DataError", "Key operations and usage mismatch"); + return NULL; + } + } + if (jwk->has_ext && !jwk->ext && extractable) { + scr_throw_domex("DataError", "JWK \"ext\" Parameter and extractable mismatch"); + return NULL; + } + ScrStr *encoding = scr_str_new("base64url", 9); + ScrBytes *bytes = scr_bytes_from_str(jwk->k, encoding); + scr_str_release(encoding); + return bytes; +} + /* ── the operations ─────────────────────────────────────────────────── * Each takes its argument vector as one dyn array (borrowed) and answers * a +1 promise. */ @@ -573,6 +877,19 @@ static ScrPromise *scr_wc_rejected(void) { return p; } +/* A promise already fulfilled with `value` (moves). */ +static ScrPromise *scr_wc_resolved(ScrDyn *value) { + return scr_promise_settled_ref(value, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); +} + +/* A promise that fulfills with `value` (moves) on a later loop turn, like + * Node's crypto jobs. */ +static ScrPromise *scr_wc_deferred(ScrDyn *value) { + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_dyn(value, NULL, scr_crypto_promise_callback(p), scr_crypto_promise_settle_dyn); + return p; +} + /* The argument count check every method starts with (after the receiver * check the compiler's call shape already guarantees). */ static bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required) { @@ -595,9 +912,158 @@ static void scr_wc_args_drop(ScrWcArgs *a, size_t n) { for (size_t i = 0; i < n; i++) scr_dyn_release(a->v[i]); } +/* AlgorithmIdentifier: an object, or a value converted to a DOMString. */ +static bool scr_wc_algorithm_identifier(ScrDyn *value) { + if (scr_wc_is_object(value)) return true; + ScrStr *s = scr_wc_dom_string(value); + if (!s) return false; + scr_str_release(s); + return true; +} + +/* macImportKey: the usage check, the format, then normalizeKeyLength. */ +static ScrDyn *scr_wc_import_hmac(const char *format, ScrBytes *data, const ScrWcJwk *jwk, + const ScrWcAlgorithm *algorithm, bool extractable, long usages, + bool *unsupported) { + if (!scr_wc_check_usages("HMAC", usages)) return NULL; + ScrBytes *bytes = NULL; + if (strcmp(format, "raw") == 0 || strcmp(format, "raw-secret") == 0) { + bytes = scr_bytes_retain(data); + } else if (strcmp(format, "jwk") == 0) { + bytes = scr_wc_jwk_secret(jwk, extractable, usages, "sig"); + if (!bytes) return NULL; + char buf[16]; + const char *expected = scr_wc_jwk_alg("HMAC", algorithm->hash, 0, buf, sizeof buf); + if (jwk->alg && expected && !scr_wc_str_is(jwk->alg, expected)) { + scr_bytes_release(bytes); + scr_throw_domex("DataError", "JWK \"alg\" does not match the requested algorithm"); + return NULL; + } + } else { + *unsupported = true; + return NULL; + } + ScrDyn *result = NULL; + { + ScrBytes *view = bytes; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + double length = (double)len * 8; + if (len == 0) { + scr_throw_domex("DataError", "Zero-length key is not supported"); + goto done; + } + if (algorithm->has_length) { + if (scr_wc_bits_to_bytes(algorithm->length) != len) { + scr_throw_domex("DataError", "Invalid key length"); + goto done; + } + length = algorithm->length; + } + result = scr_crypto_key_new(view->data, len, "HMAC", algorithm->hash, length, + (unsigned)usages, extractable); + ScrCryptoKey *key = scr_crypto_key_of(result); + scr_wc_truncate_bits(key->data, key->len, length); + } +done: + scr_bytes_release(bytes); + return result; +} + +/* aesImportKey: the usage check, then the format and the key length. */ +static ScrDyn *scr_wc_import_aes(const char *format, ScrBytes *data, const ScrWcJwk *jwk, + const char *name, bool extractable, long usages, + bool *unsupported) { + if (!scr_wc_check_usages(name, usages)) return NULL; + ScrBytes *bytes = NULL; + bool from_jwk = strcmp(format, "jwk") == 0; + if (strcmp(format, "raw") == 0 || strcmp(format, "raw-secret") == 0) { + bytes = scr_bytes_retain(data); + } else if (from_jwk) { + bytes = scr_wc_jwk_secret(jwk, extractable, usages, "enc"); + if (!bytes) return NULL; + } else { + *unsupported = true; + return NULL; + } + ScrDyn *result = NULL; + { + ScrBytes *view = bytes; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + double length = (double)len * 8; + if (length != 128 && length != 192 && length != 256) { + scr_throw_domex("DataError", "Invalid key length"); + goto done; + } + char buf[16]; + if (from_jwk && jwk->alg && + !scr_wc_str_is(jwk->alg, scr_wc_jwk_alg(name, NULL, length, buf, sizeof buf))) { + scr_throw_domex("DataError", "JWK \"alg\" does not match the requested algorithm"); + goto done; + } + result = scr_crypto_key_new(view->data, len, name, NULL, length, (unsigned)usages, extractable); + } +done: + scr_bytes_release(bytes); + return result; +} + +/* importGenericSecretKey (PBKDF2, HKDF): never extractable, derivation + * usages only, raw bytes of any length. */ +static ScrDyn *scr_wc_import_kdf(const char *format, ScrBytes *data, const char *name, + bool extractable, long usages, bool *unsupported) { + if (extractable) { + char message[64]; + snprintf(message, sizeof message, "%s keys are not extractable", name); + scr_throw_domex("SyntaxError", message); + return NULL; + } + if (!scr_wc_check_usages(name, usages)) return NULL; + if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0) { + *unsupported = true; + return NULL; + } + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + return scr_crypto_key_new(view->data, (size_t)scr_bytes_byte_len(view), name, NULL, 0, + (unsigned)usages, false); +} + +/* importKeySync over converted arguments: the per-algorithm import, the + * unsupported-format NotSupportedError, then the empty-usages check. */ +static ScrDyn *scr_wc_import_key_sync(const char *format, ScrBytes *data, const ScrWcJwk *jwk, + const ScrWcAlgorithm *algorithm, bool extractable, + long usages) { + bool unsupported = false; + ScrDyn *key; + if (strcmp(algorithm->name, "HMAC") == 0) + key = scr_wc_import_hmac(format, data, jwk, algorithm, extractable, usages, &unsupported); + else if (scr_wc_is_aes(algorithm->name)) + key = scr_wc_import_aes(format, data, jwk, algorithm->name, extractable, usages, &unsupported); + else if (scr_wc_is_kdf(algorithm->name)) + key = scr_wc_import_kdf(format, data, algorithm->name, extractable, usages, &unsupported); + else { + scr_wc_unsupported_algorithm("importKey", algorithm->name); + return NULL; + } + if (unsupported) { + char message[96]; + snprintf(message, sizeof message, "Unable to import %s using %s format", algorithm->name, + format); + scr_throw_domex("NotSupportedError", message); + return NULL; + } + if (key && usages == 0) { + scr_dyn_release(key); + scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a secret key."); + return NULL; + } + return key; +} + /* importKey(format, keyData, algorithm, extractable, keyUsages): Node's - * conversion order, then macImportKey's checks and importKeySync's empty - * usage check. */ + * conversion order, then normalization and importKeySync. */ static ScrDyn *scr_wc_import(const ScrDyn *args) { static const char prefix[] = "Failed to execute 'importKey' on 'SubtleCrypto'"; if (!scr_wc_required(prefix, args, 5)) return NULL; @@ -605,85 +1071,147 @@ static ScrDyn *scr_wc_import(const ScrDyn *args) { scr_wc_args_take(&a, args, 5); ScrDyn *result = NULL; ScrBytes *data = NULL; + ScrWcJwk jwk; + memset(&jwk, 0, sizeof jwk); const char *format = scr_wc_key_format(prefix, a.v[0]); if (!format) goto done; - bool jwk = strcmp(format, "jwk") == 0; - /* The JsonWebKey dictionary is not implemented; that format is refused - * after the remaining arguments convert. */ - if (!jwk) { + if (strcmp(format, "jwk") == 0) { + if (!scr_wc_jwk_convert(prefix, a.v[1], &jwk)) goto done; + } else { data = scr_wc_buffer_source(prefix, 1, a.v[1]); if (!data) goto done; } - if (!scr_wc_is_object(a.v[2])) { - ScrStr *s = scr_wc_dom_string(a.v[2]); - if (!s) goto done; - scr_str_release(s); - } + if (!scr_wc_algorithm_identifier(a.v[2])) goto done; bool extractable = scr_dyn_truthy(a.v[3]); long usages = scr_wc_key_usages(prefix, 4, a.v[4]); if (usages < 0) goto done; ScrWcAlgorithm algorithm; - if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), true, - &algorithm)) + if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), + SCR_WC_OP_IMPORT, &algorithm)) goto done; - if (strcmp(algorithm.name, "HMAC") != 0) { - scr_wc_unsupported_algorithm("importKey", algorithm.name); + result = scr_wc_import_key_sync(format, data, &jwk, &algorithm, extractable, usages); +done: + if (data) scr_bytes_release(data); + scr_wc_jwk_drop(&jwk); + scr_wc_args_drop(&a, 5); + return result; +} + +ScrPromise *scr_webcrypto_import_key(ScrDyn *args) { + ScrDyn *key = scr_wc_import(args); + if (!key) return scr_wc_rejected(); + return scr_wc_resolved(key); +} + +/* generateKey(algorithm, extractable, keyUsages) for HMAC and AES keys: + * random bytes of the requested length (an HMAC key defaults to its + * hash's block size; a length that is not a whole number of bytes clears + * the final byte's unused low bits). */ +ScrPromise *scr_webcrypto_generate_key(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'generateKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 3)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 3); + ScrDyn *key = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + bool extractable = scr_dyn_truthy(a.v[1]); + long usages = scr_wc_key_usages(prefix, 2, a.v[2]); + if (usages < 0) goto done; + ScrWcAlgorithm algorithm; + if (!scr_wc_normalize(a.v[0], scr_wc_key_names, SCR_WC_COUNT(scr_wc_key_names), + SCR_WC_OP_GENERATE, &algorithm)) + goto done; + bool hmac = strcmp(algorithm.name, "HMAC") == 0; + if (!hmac && !scr_wc_is_aes(algorithm.name)) { + scr_wc_unsupported_algorithm("generateKey", algorithm.name); goto done; } - if (usages & ~(long)(SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY)) { - scr_throw_domex("SyntaxError", "Unsupported key usage for HMAC key"); + if (!scr_wc_check_usages(algorithm.name, usages)) goto done; + if (usages == 0) { + scr_throw_domex("SyntaxError", "Usages cannot be empty when creating a key."); goto done; } - if (jwk) { - scr_wc_unsupported("importKey", "the 'jwk' key format"); + double length = algorithm.has_length ? algorithm.length + : strcmp(algorithm.hash, "SHA-384") == 0 || strcmp(algorithm.hash, "SHA-512") == 0 + ? 1024 + : 512; + size_t len = scr_wc_bits_to_bytes(length); + unsigned char *bytes = malloc(len ? len : 1); + if (!bytes) scr_trap("scriptc: out of memory\n"); + arc4random_buf(bytes, len); + scr_wc_truncate_bits(bytes, len, length); + key = scr_crypto_key_new(bytes, len, algorithm.name, hmac ? algorithm.hash : NULL, length, + (unsigned)usages, extractable); + volatile unsigned char *wipe = bytes; + for (size_t i = 0; i < len; i++) wipe[i] = 0; + free(bytes); +done: + scr_wc_args_drop(&a, 3); + if (!key) return scr_wc_rejected(); + return scr_wc_deferred(key); +} + +/* exportKeySync for secret keys: raw and raw-secret answer an ArrayBuffer + * of the key bytes, jwk an object { key_ops, ext, alg, kty, k }. */ +static ScrDyn *scr_wc_export(const ScrDyn *args) { + static const char prefix[] = "Failed to execute 'exportKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 2)) return NULL; + ScrWcArgs a; + scr_wc_args_take(&a, args, 2); + ScrDyn *result = NULL; + const char *format = scr_wc_key_format(prefix, a.v[0]); + if (!format) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); goto done; } - if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0) { - char message[96]; - snprintf(message, sizeof message, "Unable to import HMAC using %s format", format); + if (scr_wc_is_kdf(key->name)) { + char message[64]; + snprintf(message, sizeof message, "%s key export is not supported", key->name); scr_throw_domex("NotSupportedError", message); goto done; } - { - ScrBytes *view = data; - SCR_BYTES_SNAPSHOT(view); - size_t len = (size_t)scr_bytes_byte_len(view); - if (len == 0) { - scr_throw_domex("DataError", "Zero-length key is not supported"); - goto done; - } - double length = (double)len * 8; - unsigned char last = view->data[len - 1]; - if (algorithm.has_length) { - double bytes = floor(algorithm.length / 8) + floor((7 + fmod(algorithm.length, 8)) / 8); - if (bytes != (double)len) { - scr_throw_domex("DataError", "Invalid key length"); - goto done; - } - /* A length that is not a whole number of bytes clears the unused - * low bits of the final byte (truncateToBitLength). */ - int extra = (int)fmod(algorithm.length, 8); - if (extra) last = (unsigned char)(last & (0xff << (8 - extra))); - length = algorithm.length; - } - if (usages == 0) { - scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a secret key."); - goto done; - } - result = scr_crypto_key_new(view->data, len, algorithm.hash, length, (unsigned)usages, - extractable); - scr_crypto_key_of(result)->data[len - 1] = last; + if (!key->extractable) { + scr_throw_domex("InvalidAccessError", "key is not extractable"); + goto done; + } + if (strcmp(format, "raw") == 0 || strcmp(format, "raw-secret") == 0) { + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)key->len); + if (key->len) memcpy(bytes->data, key->data, key->len); + result = scr_array_buffer_from_bytes(bytes); + scr_bytes_release(bytes); + } else if (strcmp(format, "jwk") == 0) { + char buf[16]; + const char *alg = scr_wc_jwk_alg(key->name, key->hash, key->length, buf, sizeof buf); + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)key->len); + if (key->len) memcpy(bytes->data, key->data, key->len); + ScrStr *encoding = scr_str_new("base64url", 9); + ScrStr *k = scr_bytes_to_str(bytes, encoding); + scr_str_release(encoding); + scr_bytes_release(bytes); + result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "key_ops", 7, scr_wc_usage_array(key->usages)); + scr_dyn_obj_set(result, "ext", 3, scr_dyn_new_bool(key->extractable)); + if (alg) scr_dyn_obj_set(result, "alg", 3, scr_wc_cstr(alg)); + scr_dyn_obj_set(result, "kty", 3, scr_wc_cstr("oct")); + scr_dyn_obj_set(result, "k", 1, scr_dyn_new_str(k)); + scr_str_release(k); + } else { + char message[96]; + snprintf(message, sizeof message, "Unable to export %s secret key using %s format", key->name, + format); + scr_throw_domex("NotSupportedError", message); } done: - if (data) scr_bytes_release(data); - scr_wc_args_drop(&a, 5); + scr_wc_args_drop(&a, 2); return result; } -ScrPromise *scr_webcrypto_import_key(ScrDyn *args) { - ScrDyn *key = scr_wc_import(args); - if (!key) return scr_wc_rejected(); - return scr_promise_settled_ref(key, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); +ScrPromise *scr_webcrypto_export_key(ScrDyn *args) { + ScrDyn *result = scr_wc_export(args); + if (!result) return scr_wc_rejected(); + return scr_wc_resolved(result); } /* sign(algorithm, key, data) / verify(algorithm, key, signature, data): @@ -717,13 +1245,17 @@ static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char ma data = scr_wc_buffer_source(prefix, verify ? 3 : 2, a.v[verify ? 3 : 2]); if (!data) goto done; ScrWcAlgorithm algorithm; - if (!scr_wc_normalize(a.v[0], scr_wc_sign_names, SCR_WC_COUNT(scr_wc_sign_names), false, - &algorithm)) + if (!scr_wc_normalize(a.v[0], scr_wc_sign_names, SCR_WC_COUNT(scr_wc_sign_names), + SCR_WC_OP_SIGN, &algorithm)) goto done; if (strcmp(algorithm.name, "HMAC") != 0) { scr_wc_unsupported_algorithm(method, algorithm.name); goto done; } + if (strcmp(key->name, algorithm.name) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + goto done; + } if (!(key->usages & (verify ? SCR_WC_USAGE_VERIFY : SCR_WC_USAGE_SIGN))) { char message[64]; snprintf(message, sizeof message, "Unable to use this key to %s", method); @@ -827,3 +1359,563 @@ ScrPromise *scr_webcrypto_digest(ScrDyn *args) { scr_crypto_promise_settle_array_buffer); return p; } + +/* ── encrypt and decrypt ────────────────────────────────────────────── + * The WebCrypto half of encrypt and decrypt: argument conversion, the + * AesCbcParams, AesCtrParams and AeadParams dictionaries, and the key + * checks. The cipher itself runs in scr_webcrypto_aes.c, which links + * with the mbedTLS archive and passes it in. */ +static const char *const scr_wc_cipher_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-OCB", "ChaCha20-Poly1305", "RSA-OAEP", +}; + +/* Node's limit on the data, IV and additional data (kMaxBufferLength). */ +static const double scr_wc_max_buffer = 2147483647.0; + +/* A BufferSource member of an algorithm dictionary: false with the error + * pending; *out stays NULL when an optional member is absent. */ +static bool scr_wc_member_buffer(const ScrDyn *dict, const char *dict_name, const char *key, + bool required, ScrBytes **out) { + ScrDyn *value = scr_wc_member(dict, key); + bool ok = true; + if (value->kind == SCR_DYN_UNDEF) { + if (required) { + scr_wc_missing_member(dict_name, key); + ok = false; + } + } else { + ScrDyn *plain = scr_wc_plain(value); + if (!plain) ok = false; + else if (plain->kind == SCR_DYN_BYTES || scr_array_buffer_is(plain)) { + *out = scr_bytes_buffer_source(plain); + } else { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: %s in passed algorithm is not instance of ArrayBuffer, Buffer, " + "TypedArray, or DataView.", + scr_wc_normalize_prefix, key); + ok = false; + } + if (plain) scr_dyn_release(plain); + } + scr_dyn_release(value); + return ok; +} + +/* validateByteLength / validateMaxBufferLength over a converted member. */ +static bool scr_wc_exact_length(const ScrBytes *bytes, const char *name, double length) { + if (scr_bytes_byte_len(bytes) == length) return true; + char message[96]; + snprintf(message, sizeof message, "%s must contain exactly %.0f bytes", name, length); + scr_throw_domex("OperationError", message); + return false; +} + +static bool scr_wc_max_length(const ScrBytes *bytes, const char *name) { + if (scr_bytes_byte_len(bytes) <= scr_wc_max_buffer) return true; + char message[96]; + snprintf(message, sizeof message, "%s must be at most %.0f bytes", name, scr_wc_max_buffer); + scr_throw_domex("OperationError", message); + return false; +} + +typedef struct { + ScrBytes *iv; /* the IV or the CTR counter block */ + ScrBytes *aad; + double tag_bits; + double counter_bits; +} ScrWcCipherParams; + +static void scr_wc_cipher_params_drop(ScrWcCipherParams *params) { + if (params->iv) scr_bytes_release(params->iv); + if (params->aad) scr_bytes_release(params->aad); + params->iv = params->aad = NULL; +} + +/* An optional or required [EnforceRange] octet member: false with the + * error pending; *present says whether it was given. */ +static bool scr_wc_member_octet(const ScrDyn *dict, const char *dict_name, const char *key, + bool required, bool *present, double *out) { + ScrDyn *value = scr_wc_member(dict, key); + bool ok = true; + *present = value->kind != SCR_DYN_UNDEF; + if (!*present) { + if (required) { + scr_wc_missing_member(dict_name, key); + ok = false; + } + } else { + char context[48]; + snprintf(context, sizeof context, "%s in passed algorithm", key); + ok = scr_wc_enforce_range(scr_wc_normalize_prefix, context, value, 255, out); + } + scr_dyn_release(value); + return ok; +} + +/* normalizeAlgorithm(algorithm, 'encrypt' / 'decrypt'): the canonical + * name and the AES dictionaries' members, in Web IDL member order with + * each member's validator right after its conversion. */ +static const char *scr_wc_normalize_cipher(ScrDyn *identifier, const char *method, + ScrWcCipherParams *params) { + memset(params, 0, sizeof *params); + params->tag_bits = 128; + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return NULL; + const char *canonical = + scr_wc_lookup(scr_wc_cipher_names, SCR_WC_COUNT(scr_wc_cipher_names), name); + scr_str_release(name); + if (!canonical) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return NULL; + } + if (!scr_wc_is_aes(canonical)) { + scr_wc_unsupported_algorithm(method, canonical); + return NULL; + } + ScrDyn *dict = scr_wc_is_object(identifier) ? scr_wc_plain(identifier) + : scr_dyn_retain(scr_dyn_undefined()); + if (!dict) return NULL; + bool ok = true; + bool present; + if (strcmp(canonical, "AES-CBC") == 0) { + ok = scr_wc_member_buffer(dict, "AesCbcParams", "iv", true, ¶ms->iv) && + scr_wc_exact_length(params->iv, "algorithm.iv", 16); + } else if (strcmp(canonical, "AES-CTR") == 0) { + ok = scr_wc_member_buffer(dict, "AesCtrParams", "counter", true, ¶ms->iv) && + scr_wc_exact_length(params->iv, "algorithm.counter", 16) && + scr_wc_member_octet(dict, "AesCtrParams", "length", true, &present, + ¶ms->counter_bits); + if (ok && (params->counter_bits == 0 || params->counter_bits > 128)) { + scr_throw_domex("OperationError", "AES-CTR algorithm.length must be between 1 and 128"); + ok = false; + } + } else { + ok = scr_wc_member_buffer(dict, "AeadParams", "additionalData", false, ¶ms->aad) && + (!params->aad || scr_wc_max_length(params->aad, "algorithm.additionalData")) && + scr_wc_member_buffer(dict, "AeadParams", "iv", true, ¶ms->iv) && + scr_wc_max_length(params->iv, "algorithm.iv") && + scr_wc_member_octet(dict, "AeadParams", "tagLength", false, &present, ¶ms->tag_bits); + if (ok && !present) params->tag_bits = 128; + double t = params->tag_bits; + if (ok && t != 32 && t != 64 && t != 96 && t != 104 && t != 112 && t != 120 && t != 128) { + char message[64]; + snprintf(message, sizeof message, "%.0f is not a valid AES-GCM tag length", t); + scr_throw_domex("OperationError", message); + ok = false; + } + } + scr_dyn_release(dict); + if (!ok) { + scr_wc_cipher_params_drop(params); + return NULL; + } + return canonical; +} + +static void *scr_wc_caught_retain(void *c) { return scr_caught_retain(c); } +static void scr_wc_caught_release(void *c) { scr_caught_release(c); } + +static void scr_wc_settle_rethrow(ScrClosure *cb, ScrError *error, ScrBytes *value) { + (void)error; + (void)value; + ScrPromise *p; + ScrCaught *caught; + memcpy(&p, &cb->caps[0]->slot, sizeof p); + memcpy(&caught, &cb->caps[1]->slot, sizeof caught); + scr_rethrow(caught); + scr_promise_reject_pending(p); +} + +/* A job failure: a fresh promise that rejects with the pending exception + * on a later loop turn, as Node's job promises do (+1). */ +static ScrPromise *scr_wc_deferred_rejection(void) { + ScrPromise *p = scr_promise_new(); + ScrClosure *cb = scr_closure_new(NULL, 2); + cb->caps[0] = scr_box_new_obj(scr_promise_retain_v, scr_promise_release_v, NULL); + scr_box_set_ref(cb->caps[0], scr_promise_retain(p)); + cb->caps[1] = scr_box_new_obj(scr_wc_caught_retain, scr_wc_caught_release, scr_caught_trace_v); + scr_box_set_ref(cb->caps[1], scr_exc_take()); + scr_crypto_defer_bytes(NULL, NULL, cb, scr_wc_settle_rethrow); + return p; +} + +/* A promise that fulfills with an ArrayBuffer of `bytes` (moves) on a + * later loop turn. */ +static ScrPromise *scr_wc_deferred_buffer(ScrBytes *bytes) { + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_bytes(bytes, NULL, scr_crypto_promise_callback(p), + scr_crypto_promise_settle_array_buffer); + return p; +} + +ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn aes) { + const char *method = decrypt ? "decrypt" : "encrypt"; + const char *prefix = decrypt ? "Failed to execute 'decrypt' on 'SubtleCrypto'" + : "Failed to execute 'encrypt' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 3)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 3); + ScrBytes *data = NULL; + ScrWcCipherParams params; + memset(¶ms, 0, sizeof params); + ScrPromise *result = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + data = scr_wc_buffer_source(prefix, 2, a.v[2]); + if (!data) goto done; + const char *name = scr_wc_normalize_cipher(a.v[0], method, ¶ms); + if (!name) goto done; + if (strcmp(key->name, name) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + goto done; + } + if (!(key->usages & (decrypt ? SCR_WC_USAGE_DECRYPT : SCR_WC_USAGE_ENCRYPT))) { + char message[64]; + snprintf(message, sizeof message, "Unable to use this key to %s", method); + scr_throw_domex("InvalidAccessError", message); + goto done; + } + if (!scr_wc_max_length(data, "data")) goto done; + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + ScrBytes *iv = params.iv; + SCR_BYTES_SNAPSHOT(iv); + if (!params.aad) params.aad = scr_bytes_new(SCR_BYTES_U8, 0); + ScrBytes *aad = params.aad; + SCR_BYTES_SNAPSHOT(aad); + ScrWebCryptoAesParams run = { + .iv = iv->data, + .iv_len = (size_t)scr_bytes_byte_len(iv), + .aad = aad->data, + .aad_len = (size_t)scr_bytes_byte_len(aad), + .tag_len = (size_t)(params.tag_bits / 8), + .counter_bits = (unsigned)params.counter_bits, + }; + ScrBytes *out = aes(name, decrypt, key->data, key->len, &run, view->data, + (size_t)scr_bytes_byte_len(view)); + if (out) { + result = scr_wc_deferred_buffer(out); + } else { + scr_throw_domex("OperationError", "The operation failed for an operation-specific reason"); + result = scr_wc_deferred_rejection(); + } + } +done: + if (data) scr_bytes_release(data); + scr_wc_cipher_params_drop(¶ms); + scr_wc_args_drop(&a, 3); + return result ? result : scr_wc_rejected(); +} + +/* ── deriveBits and deriveKey ───────────────────────────────────────── + * PBKDF2 and HKDF over node:crypto's KDF core (scr_crypto_pbkdf2 and + * scr_crypto_hkdf_bytes in scr_lib.c), with Node's Pbkdf2Params and + * HkdfParams dictionaries, length rules and key checks. deriveKey derives + * the bits the derived key type needs and imports them as a raw-secret + * key of that type. */ +static const char *const scr_wc_derive_names[] = { + "Argon2d", "Argon2i", "Argon2id", "ECDH", "HKDF", "PBKDF2", "X25519", "X448", +}; +/* The algorithms 'get key length' registers. */ +static const char *const scr_wc_length_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", "Argon2d", "Argon2i", + "Argon2id", "ChaCha20-Poly1305", "HKDF", "HMAC", "KMAC128", "KMAC256", "PBKDF2", +}; + +typedef struct { + const char *name; + const char *hash; + double iterations; + ScrBytes *salt; + ScrBytes *info; +} ScrWcKdfParams; + +static void scr_wc_kdf_params_drop(ScrWcKdfParams *params) { + if (params->salt) scr_bytes_release(params->salt); + if (params->info) scr_bytes_release(params->info); + params->salt = params->info = NULL; +} + +/* A HashAlgorithmIdentifier member with ensureSHA (`dict` names the + * dictionary); *hash is the member value for the later digest + * normalization (+1). */ +static bool scr_wc_member_hash(const ScrDyn *dict, const char *dict_name, ScrDyn **hash) { + ScrDyn *value = scr_wc_member(dict, "hash"); + if (value->kind == SCR_DYN_UNDEF) { + scr_dyn_release(value); + scr_wc_missing_member(dict_name, "hash"); + return false; + } + ScrStr *name = NULL; + bool ok = true; + if (scr_wc_is_object(value)) { + ScrDyn *plain = scr_wc_plain(value); + ScrDyn *member = plain ? scr_wc_member(plain, "name") : NULL; + if (plain) scr_dyn_release(plain); + if (!member) ok = false; + else { + if (member->kind == SCR_DYN_STR) name = scr_str_retain(member->v.str); + scr_dyn_release(member); + } + *hash = scr_dyn_retain(value); + } else { + name = scr_wc_dom_string(value); + if (!name) ok = false; + *hash = name ? scr_dyn_new_str(name) : NULL; + } + scr_dyn_release(value); + bool sha = name && name->len >= 3 && (name->data[0] | 32) == 's' && + (name->data[1] | 32) == 'h' && (name->data[2] | 32) == 'a'; + if (name) scr_str_release(name); + if (ok && !sha) { + char message[96]; + snprintf(message, sizeof message, "Only SHA hashes are supported in %s", dict_name); + scr_throw_domex("NotSupportedError", message); + ok = false; + } + if (!ok && *hash) { + scr_dyn_release(*hash); + *hash = NULL; + } + return ok; +} + +/* normalizeAlgorithm(algorithm, 'deriveBits') for PBKDF2 and HKDF. */ +static bool scr_wc_normalize_kdf(ScrDyn *identifier, const char *method, ScrWcKdfParams *params) { + memset(params, 0, sizeof *params); + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return false; + params->name = scr_wc_lookup(scr_wc_derive_names, SCR_WC_COUNT(scr_wc_derive_names), name); + scr_str_release(name); + if (!params->name) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return false; + } + if (!scr_wc_is_kdf(params->name)) { + scr_wc_unsupported_algorithm(method, params->name); + return false; + } + bool pbkdf2 = strcmp(params->name, "PBKDF2") == 0; + const char *dict_name = pbkdf2 ? "Pbkdf2Params" : "HkdfParams"; + ScrDyn *dict = scr_wc_is_object(identifier) ? scr_wc_plain(identifier) + : scr_dyn_retain(scr_dyn_undefined()); + if (!dict) return false; + ScrDyn *hash = NULL; + bool ok = scr_wc_member_hash(dict, dict_name, &hash); + if (ok && pbkdf2) { + ScrDyn *iterations = scr_wc_member(dict, "iterations"); + if (iterations->kind == SCR_DYN_UNDEF) { + scr_wc_missing_member(dict_name, "iterations"); + ok = false; + } else if (!scr_wc_enforce_range(scr_wc_normalize_prefix, "iterations in passed algorithm", + iterations, 4294967295.0, ¶ms->iterations)) { + ok = false; + } else if (params->iterations == 0) { + scr_throw_domex("OperationError", "iterations cannot be zero"); + ok = false; + } else if (params->iterations > 2147483647.0) { + scr_throw_domex("NotSupportedError", "iterations exceeds the implementation limit"); + ok = false; + } + scr_dyn_release(iterations); + ok = ok && scr_wc_member_buffer(dict, dict_name, "salt", true, ¶ms->salt); + } else if (ok) { + ok = scr_wc_member_buffer(dict, dict_name, "info", true, ¶ms->info); + if (ok && scr_bytes_byte_len(params->info) > 1024) { + scr_throw_domex("OperationError", "algorithm.info must be at most 1024 bytes"); + ok = false; + } + ok = ok && scr_wc_member_buffer(dict, dict_name, "salt", true, ¶ms->salt); + } + scr_dyn_release(dict); + if (ok) { + params->hash = scr_wc_digest_algorithm(hash, method); + if (!params->hash) ok = false; + } + if (hash) scr_dyn_release(hash); + if (!ok) scr_wc_kdf_params_drop(params); + return ok; +} + +/* The length rules of pbkdf2DeriveBits and hkdfDeriveBits; `null_length` + * says the length was null. */ +static bool scr_wc_kdf_length(const ScrWcKdfParams *params, bool null_length, double length) { + if (null_length) { + scr_throw_domex("OperationError", "length cannot be null"); + return false; + } + if (fmod(length, 8) != 0) { + scr_throw_domex("OperationError", "length must be a multiple of 8"); + return false; + } + if (strcmp(params->name, "HKDF") == 0) { + double digest = strcmp(params->hash, "SHA-1") == 0 ? 20 + : strcmp(params->hash, "SHA-256") == 0 ? 32 + : strcmp(params->hash, "SHA-384") == 0 ? 48 + : 64; + if (length > 255 * digest * 8) { + scr_throw_domex("OperationError", "length exceeds the maximum derived bit length"); + return false; + } + } + return true; +} + +/* The derived bytes (+1); length is a nonzero multiple of 8. */ +static ScrBytes *scr_wc_kdf_derive(const ScrWcKdfParams *params, const ScrCryptoKey *key, + double length) { + ScrBytes *secret = scr_bytes_new(SCR_BYTES_U8, (double)key->len); + if (key->len) memcpy(secret->data, key->data, key->len); + const char *native = scr_wc_native_hash(params->hash); + ScrStr *digest = scr_str_new(native, strlen(native)); + ScrBytes *out; + if (strcmp(params->name, "PBKDF2") == 0) { + out = scr_crypto_pbkdf2(secret, params->salt, params->iterations, length / 8, digest); + } else { + out = scr_crypto_hkdf_bytes(digest, secret, params->salt, params->info, length / 8); + } + scr_str_release(digest); + volatile unsigned char *wipe = secret->data; + for (size_t i = 0; i < key->len; i++) wipe[i] = 0; + scr_bytes_release(secret); + return out; +} + +/* The base key checks deriveBits and deriveKey share. */ +static bool scr_wc_check_base_key(const ScrCryptoKey *key, const char *algorithm, unsigned usage, + const char *usage_name) { + if (!(key->usages & usage)) { + char message[64]; + snprintf(message, sizeof message, "baseKey does not have %s usage", usage_name); + scr_throw_domex("InvalidAccessError", message); + return false; + } + if (strcmp(key->name, algorithm) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + return false; + } + return true; +} + +/* deriveBits(algorithm, baseKey[, length]). */ +ScrPromise *scr_webcrypto_derive_bits(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'deriveBits' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 2)) return scr_wc_rejected(); + size_t argc = (size_t)scr_dyn_arr_len(args); + ScrWcArgs a; + size_t n = argc > 3 ? 3 : argc; + scr_wc_args_take(&a, args, n); + ScrWcKdfParams params; + memset(¶ms, 0, sizeof params); + ScrPromise *result = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + /* length defaults to null when absent or undefined. */ + bool null_length = n < 3 || a.v[2]->kind == SCR_DYN_UNDEF || a.v[2]->kind == SCR_DYN_NULL; + double length = 0; + if (!null_length && !scr_wc_enforce_range(prefix, "3rd argument", a.v[2], 4294967295.0, &length)) + goto done; + if (!scr_wc_normalize_kdf(a.v[0], "deriveBits", ¶ms)) goto done; + if (!scr_wc_check_base_key(key, params.name, SCR_WC_USAGE_DERIVE_BITS, "deriveBits")) goto done; + if (!scr_wc_kdf_length(¶ms, null_length, length)) goto done; + if (length == 0) { + ScrBytes *empty = scr_bytes_new(SCR_BYTES_U8, 0); + result = scr_wc_resolved(scr_array_buffer_from_bytes(empty)); + scr_bytes_release(empty); + goto done; + } + ScrBytes *bits = scr_wc_kdf_derive(¶ms, key, length); + if (bits) result = scr_wc_deferred_buffer(bits); +done: + scr_wc_kdf_params_drop(¶ms); + scr_wc_args_drop(&a, n); + return result ? result : scr_wc_rejected(); +} + +/* getKeyLength for the derived key type: the bits to derive, or *null + * for the KDF types (whose derivation then rejects). */ +static bool scr_wc_derived_length(ScrDyn *identifier, const ScrWcAlgorithm *imported, + bool *null_length, double *length) { + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return false; + const char *canonical = + scr_wc_lookup(scr_wc_length_names, SCR_WC_COUNT(scr_wc_length_names), name); + scr_str_release(name); + if (!canonical) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return false; + } + *null_length = false; + if (scr_wc_is_aes(canonical)) { + ScrWcAlgorithm aes; + memset(&aes, 0, sizeof aes); + if (!scr_wc_aes_length_params(identifier, "AesDerivedKeyParams", &aes)) return false; + *length = aes.length; + return true; + } + if (strcmp(canonical, "HMAC") == 0 && imported->hash) { + *length = imported->has_length ? imported->length + : strcmp(imported->hash, "SHA-384") == 0 || strcmp(imported->hash, "SHA-512") == 0 + ? 1024 + : 512; + return true; + } + *null_length = true; + return true; +} + +/* deriveKey(algorithm, baseKey, derivedKeyType, extractable, keyUsages): + * the derivation's errors reject at once; the import of the derived bits + * settles, or rejects, on a later loop turn. */ +ScrPromise *scr_webcrypto_derive_key(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'deriveKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 5)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 5); + ScrWcKdfParams params; + memset(¶ms, 0, sizeof params); + ScrPromise *result = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + if (!scr_wc_algorithm_identifier(a.v[2])) goto done; + bool extractable = scr_dyn_truthy(a.v[3]); + long usages = scr_wc_key_usages(prefix, 4, a.v[4]); + if (usages < 0) goto done; + if (!scr_wc_normalize_kdf(a.v[0], "deriveKey", ¶ms)) goto done; + ScrWcAlgorithm imported; + if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), + SCR_WC_OP_IMPORT, &imported)) + goto done; + bool null_length; + double length; + if (!scr_wc_derived_length(a.v[2], &imported, &null_length, &length)) goto done; + if (strcmp(imported.name, "HMAC") != 0 && !scr_wc_is_aes(imported.name) && + !scr_wc_is_kdf(imported.name)) { + scr_wc_unsupported_algorithm("deriveKey", imported.name); + goto done; + } + if (!scr_wc_check_base_key(key, params.name, SCR_WC_USAGE_DERIVE_KEY, "deriveKey")) goto done; + if (!scr_wc_kdf_length(¶ms, null_length, length)) goto done; + ScrBytes *bits = scr_wc_kdf_derive(¶ms, key, length); + if (!bits) goto done; + ScrDyn *derived = scr_wc_import_key_sync("raw-secret", bits, NULL, &imported, extractable, usages); + scr_bytes_release(bits); + result = derived ? scr_wc_deferred(derived) : scr_wc_deferred_rejection(); +done: + scr_wc_kdf_params_drop(¶ms); + scr_wc_args_drop(&a, 5); + return result ? result : scr_wc_rejected(); +} diff --git a/packages/runtime/src/scr_webcrypto_aes.c b/packages/runtime/src/scr_webcrypto_aes.c new file mode 100644 index 0000000000..7cb63f561f --- /dev/null +++ b/packages/runtime/src/scr_webcrypto_aes.c @@ -0,0 +1,137 @@ +/* WebCrypto's AES ciphers: crypto.subtle.encrypt and decrypt with AES-CBC, + * AES-CTR and AES-GCM keys, over the vendored mbedTLS block cipher. + * + * scr_webcrypto.c converts and validates the arguments, normalizes the + * algorithm and checks the key; this unit runs the cipher itself, the way + * Node 24's src/crypto/crypto_aes.cc does: CBC with PKCS#7 padding, CTR + * with a counter that wraps within its low `length` bits (and fails when + * the input needs more blocks than those bits can count), and GCM with a + * 12 to 128 byte IV and a 4 to 16 byte tag appended to the ciphertext. + * The unit links only into programs that call these methods, alongside + * the cipher feature's mbedTLS archive. */ +#include "scr_runtime.h" + +#include +#include + +#include "mbedtls/aes.h" +#include "mbedtls/gcm.h" +#include "mbedtls/platform_util.h" + +static ScrBytes *scr_wc_aes_gcm(bool decrypt, const unsigned char *key, size_t key_len, + const ScrWebCryptoAesParams *p, const unsigned char *in, + size_t len) { + if (p->iv_len < 12 || p->iv_len > 128) return NULL; + if (decrypt && len < p->tag_len) return NULL; + size_t out_len = decrypt ? len - p->tag_len : len + p->tag_len; + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)out_len); + mbedtls_gcm_context gcm; + mbedtls_gcm_init(&gcm); + int rc = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, (unsigned)(key_len * 8)); + if (rc == 0 && decrypt) { + rc = mbedtls_gcm_auth_decrypt(&gcm, out_len, p->iv, p->iv_len, p->aad, p->aad_len, + in + out_len, p->tag_len, in, out->data); + } else if (rc == 0) { + rc = mbedtls_gcm_crypt_and_tag(&gcm, MBEDTLS_GCM_ENCRYPT, len, p->iv, p->iv_len, p->aad, + p->aad_len, in, out->data, p->tag_len, out->data + len); + } + mbedtls_gcm_free(&gcm); + if (rc != 0) { + mbedtls_platform_zeroize(out->data, out_len); + scr_bytes_release(out); + return NULL; + } + return out; +} + +static ScrBytes *scr_wc_aes_cbc(bool decrypt, const unsigned char *key, size_t key_len, + const ScrWebCryptoAesParams *p, const unsigned char *in, + size_t len) { + if (decrypt && (len == 0 || len % 16 != 0)) return NULL; + size_t padded = decrypt ? len : (len / 16 + 1) * 16; + unsigned char *buf = malloc(padded); + if (!buf) scr_trap("scriptc: out of memory\n"); + memcpy(buf, in, len); + if (!decrypt) memset(buf + len, (int)(padded - len), padded - len); + unsigned char iv[16]; + memcpy(iv, p->iv, 16); + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + int rc = decrypt ? mbedtls_aes_setkey_dec(&aes, key, (unsigned)(key_len * 8)) + : mbedtls_aes_setkey_enc(&aes, key, (unsigned)(key_len * 8)); + if (rc == 0) + rc = mbedtls_aes_crypt_cbc(&aes, decrypt ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, padded, + iv, buf, buf); + mbedtls_aes_free(&aes); + size_t out_len = padded; + if (rc == 0 && decrypt) { + /* PKCS#7: the final byte names 1 to 16 bytes of padding, each of + * which holds that value. Checked without data-dependent branches. */ + unsigned char pad = buf[padded - 1]; + unsigned char diff = (unsigned char)-(pad == 0 || pad > 16); + for (unsigned i = 1; i <= 16; i++) + diff |= (unsigned char)((buf[padded - i] ^ pad) & (unsigned char)-(i <= pad)); + if (diff) rc = -1; + else out_len = padded - pad; + } + ScrBytes *out = NULL; + if (rc == 0) { + out = scr_bytes_new(SCR_BYTES_U8, (double)out_len); + memcpy(out->data, buf, out_len); + } + mbedtls_platform_zeroize(buf, padded); + free(buf); + return out; +} + +/* Adds one to the low `bits` bits of a big-endian counter block, leaving + * the bits above them unchanged. */ +static void scr_wc_ctr_increment(unsigned char block[16], unsigned bits) { + for (int i = 15; i >= 0 && bits > 0; i--) { + unsigned width = bits >= 8 ? 8 : bits; + unsigned mask = (1u << width) - 1; + unsigned low = ((unsigned)block[i] + 1) & mask; + block[i] = (unsigned char)((block[i] & ~mask) | low); + bits -= width; + if (low != 0) return; + } +} + +static ScrBytes *scr_wc_aes_ctr(const unsigned char *key, size_t key_len, + const ScrWebCryptoAesParams *p, const unsigned char *in, + size_t len) { + size_t blocks = (len + 15) / 16; + if (p->counter_bits < 64 && (double)blocks > (double)(1ull << p->counter_bits)) return NULL; + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + if (mbedtls_aes_setkey_enc(&aes, key, (unsigned)(key_len * 8)) != 0) { + mbedtls_aes_free(&aes); + return NULL; + } + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)len); + unsigned char counter[16], stream[16]; + memcpy(counter, p->iv, 16); + for (size_t off = 0; off < len; off += 16) { + mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, counter, stream); + size_t n = len - off < 16 ? len - off : 16; + for (size_t i = 0; i < n; i++) out->data[off + i] = (unsigned char)(in[off + i] ^ stream[i]); + scr_wc_ctr_increment(counter, p->counter_bits); + } + mbedtls_platform_zeroize(stream, sizeof stream); + mbedtls_aes_free(&aes); + return out; +} + +/* The cipher scr_webcrypto_cipher runs: +1 output bytes, or NULL when the + * operation fails (Node's OperationError). */ +static ScrBytes *scr_wc_aes(const char *name, bool decrypt, const unsigned char *key, + size_t key_len, const ScrWebCryptoAesParams *p, + const unsigned char *in, size_t len) { + if (strcmp(name, "AES-GCM") == 0) return scr_wc_aes_gcm(decrypt, key, key_len, p, in, len); + if (strcmp(name, "AES-CBC") == 0) return scr_wc_aes_cbc(decrypt, key, key_len, p, in, len); + return scr_wc_aes_ctr(key, key_len, p, in, len); +} + +ScrPromise *scr_webcrypto_encrypt(ScrDyn *args) { return scr_webcrypto_cipher(args, false, scr_wc_aes); } + +ScrPromise *scr_webcrypto_decrypt(ScrDyn *args) { return scr_webcrypto_cipher(args, true, scr_wc_aes); } diff --git a/packages/runtime/src/scr_webcrypto_object.c b/packages/runtime/src/scr_webcrypto_object.c new file mode 100644 index 0000000000..c20ef13dc6 --- /dev/null +++ b/packages/runtime/src/scr_webcrypto_object.c @@ -0,0 +1,53 @@ +/* The crypto global as a value: node:crypto's dynamic Crypto object + * (scr_crypto_native in scr_async_dyn.c, which carries subtle.digest) with + * the Crypto object's own randomUUID and getRandomValues methods, for + * untyped reads such as uuid's `crypto.randomUUID && crypto.randomUUID()`. + * Node's receiver check (ERR_INVALID_THIS on a detached call) is not + * modeled. There is one object per thread, so every spelling of the + * global (crypto, globalThis.crypto, node:crypto's webcrypto) is the same + * value; it is released at exit. Linked only into programs that read the + * global as a value. */ +#include "scr_runtime.h" + +static ScrDyn *scr_webcrypto_random_uuid_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + (void)args; + (void)argc; + ScrStr *uuid = scr_crypto_random_uuid(); + ScrDyn *result = scr_dyn_new_str(uuid); + scr_str_release(uuid); + return result; +} + +static ScrDyn *scr_webcrypto_get_random_values_call(ScrClosure *closure, ScrDyn *const *args, + size_t argc) { + (void)closure; + if (argc == 0) { + static const char message[] = + "Failed to execute 'getRandomValues' on 'Crypto': 1 argument required, but only 0 present."; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_MISSING_ARGS"); + return NULL; + } + return scr_crypto_get_random_values_dyn(args[0]); +} + +static SCR_TL ScrDyn *scr_webcrypto_object_value; + +static void scr_webcrypto_object_cleanup(void) { + if (scr_webcrypto_object_value) scr_dyn_release(scr_webcrypto_object_value); + scr_webcrypto_object_value = NULL; +} + +ScrDyn *scr_webcrypto_object(void) { + if (scr_webcrypto_object_value) return scr_dyn_retain(scr_webcrypto_object_value); + ScrDyn *result = scr_crypto_native(); + scr_dyn_obj_set(result, "randomUUID", 10, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_webcrypto_random_uuid_call, 0, + "crypto.randomUUID", "randomUUID")); + scr_dyn_obj_set(result, "getRandomValues", 15, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_webcrypto_get_random_values_call, 1, + "crypto.getRandomValues", "getRandomValues")); + scr_webcrypto_object_value = result; + scr_atexit(scr_webcrypto_object_cleanup); + return scr_dyn_retain(result); +} diff --git a/tests/corpus/7120-webcrypto-random-uuid.ts b/tests/corpus/7120-webcrypto-random-uuid.ts new file mode 100644 index 0000000000..d35df97cc8 --- /dev/null +++ b/tests/corpus/7120-webcrypto-random-uuid.ts @@ -0,0 +1,22 @@ +// crypto.randomUUID() on the WebCrypto global, directly and through +// globalThis: version-4 UUIDs, distinct on every call, and inside +// closures and default parameters the way request-id middleware uses it. +const shape = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +const direct: string = crypto.randomUUID(); +console.log(direct.length, shape.test(direct)); +const viaGlobalThis = globalThis.crypto.randomUUID(); +console.log(viaGlobalThis.length, shape.test(viaGlobalThis), viaGlobalThis !== direct); + +const seen = new Set(); +for (let i = 0; i < 100; i++) seen.add(crypto.randomUUID()); +console.log("distinct", seen.size); + +function requestId(generator: () => string = () => crypto.randomUUID()): string { + return generator(); +} +console.log(shape.test(requestId()), requestId(() => "fixed")); + +const ids = ["a", "b", "c"].map((name) => ({ name, id: crypto.randomUUID() })); +console.log(ids.every((entry) => shape.test(entry.id)), ids[0]!.id !== ids[1]!.id); +console.log(typeof crypto.randomUUID()); diff --git a/tests/corpus/7121-webcrypto-crypto-object.mjs b/tests/corpus/7121-webcrypto-crypto-object.mjs new file mode 100644 index 0000000000..058e721534 --- /dev/null +++ b/tests/corpus/7121-webcrypto-crypto-object.mjs @@ -0,0 +1,35 @@ +// The crypto global from JavaScript: uuid's feature test +// (`crypto.randomUUID && crypto.randomUUID()`), the methods read as values, +// and getRandomValues through the dynamic object, including its +// missing-argument and non-integer-view errors. +const shape = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +function v4(options) { + if (!options && crypto.randomUUID) { + return crypto.randomUUID(); + } + return "fallback"; +} +console.log(shape.test(v4()), v4({})); +console.log(shape.test(globalThis.crypto.randomUUID())); +console.log(typeof crypto.randomUUID, typeof crypto.getRandomValues, typeof crypto.subtle); +console.log(crypto.randomUUID.name, crypto.getRandomValues.name, crypto.getRandomValues.length); + +const native = typeof crypto !== "undefined" && crypto.randomUUID ? { randomUUID: crypto.randomUUID.bind(crypto) } : {}; +console.log(shape.test(native.randomUUID())); + +const rng = (n) => crypto.getRandomValues(new Uint8Array(n)); +console.log(rng(16).length, rng(16) instanceof Uint8Array); +const words = new Uint32Array(4); +console.log(crypto.getRandomValues(words) === words); + +function attempt(label, run) { + try { + console.log(label, run()); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +attempt("no argument", () => crypto.getRandomValues()); +attempt("float view", () => crypto.getRandomValues(new Float64Array(2))); +attempt("too long", () => crypto.getRandomValues(new Uint8Array(65537))); diff --git a/tests/corpus/7122-webcrypto-node-crypto.ts b/tests/corpus/7122-webcrypto-node-crypto.ts new file mode 100644 index 0000000000..c0ada79395 --- /dev/null +++ b/tests/corpus/7122-webcrypto-node-crypto.ts @@ -0,0 +1,38 @@ +// WebCrypto reached through node:crypto: the webcrypto export (named and +// through the namespace), the module's own subtle export, and their +// identity with the crypto global. Each form runs the same HMAC and +// digest calls as the global. +import * as nodeCrypto from "node:crypto"; +import { subtle, webcrypto } from "node:crypto"; + +const encoder = new TextEncoder(); +const message = encoder.encode("node:crypto webcrypto"); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +console.log(webcrypto === crypto, webcrypto === globalThis.crypto, nodeCrypto.webcrypto === crypto); +console.log(hex(await webcrypto.subtle.digest("SHA-256", message))); +console.log(hex(await subtle.digest("SHA-1", message))); +console.log(hex(await nodeCrypto.subtle.digest({ name: "SHA-384" }, message))); +console.log(hex(await nodeCrypto.webcrypto.subtle.digest("SHA-512", message))); + +const secret = encoder.encode("shared secret"); +const params = { name: "HMAC", hash: "SHA-256" }; +const signer = await webcrypto.subtle.importKey("raw", secret, params, false, ["sign"]); +const verifier = await subtle.importKey("raw", secret, params, false, ["verify"]); +const signature = await nodeCrypto.subtle.sign("HMAC", signer, message); +console.log(hex(signature)); +console.log(await nodeCrypto.webcrypto.subtle.verify("HMAC", verifier, signature, message)); +console.log(await subtle.verify("HMAC", verifier, signature, encoder.encode("other"))); +console.log(signer instanceof CryptoKey, signer.algorithm.name, verifier.usages.join(",")); + +const shape = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +console.log(shape.test(webcrypto.randomUUID()), shape.test(nodeCrypto.webcrypto.randomUUID())); +console.log(webcrypto.getRandomValues(new Uint32Array(4)).length); +try { + await subtle.digest("MD5", message); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} diff --git a/tests/corpus/7123-webcrypto-node-crypto.cjs b/tests/corpus/7123-webcrypto-node-crypto.cjs new file mode 100644 index 0000000000..b11131d4b6 --- /dev/null +++ b/tests/corpus/7123-webcrypto-node-crypto.cjs @@ -0,0 +1,31 @@ +// node:crypto's WebCrypto from CommonJS: destructured webcrypto and +// subtle, the module object's members, the +// `globalThis.crypto ?? require("crypto").webcrypto` fallback libraries +// use to find a Crypto object, and crypto destructured from globalThis, +// all the same object. +const nodeCrypto = require("node:crypto"); +const { webcrypto, subtle } = require("crypto"); + +const encoder = new TextEncoder(); +const hex = (buffer) => Buffer.from(new Uint8Array(buffer)).toString("hex"); + +function cryptoObject() { + return globalThis.crypto ?? nodeCrypto.webcrypto; +} + +async function main() { + console.log(webcrypto === globalThis.crypto, cryptoObject() === crypto, typeof subtle); + console.log(hex(await webcrypto.subtle.digest("SHA-256", encoder.encode("abc")))); + console.log(hex(await subtle.digest("SHA-1", encoder.encode("abc")))); + console.log(hex(await nodeCrypto.subtle.digest("SHA-256", encoder.encode("abc")))); + const key = await nodeCrypto.webcrypto.subtle.importKey("raw", encoder.encode("k"), { name: "HMAC", hash: "SHA-1" }, true, ["sign"]); + console.log(hex(await subtle.sign("HMAC", key, encoder.encode("abc")))); + const wc = cryptoObject(); + console.log(wc.randomUUID().length, wc.getRandomValues(new Uint8Array(8)).length); + console.log(typeof webcrypto.randomUUID, typeof nodeCrypto.webcrypto.getRandomValues); + const { crypto: destructured } = globalThis; + console.log(destructured === webcrypto, destructured.randomUUID().length); + console.log(hex(await destructured.subtle.digest("SHA-1", encoder.encode("abc")))); +} + +main(); diff --git a/tests/corpus/7124-webcrypto-secret-keys.ts b/tests/corpus/7124-webcrypto-secret-keys.ts new file mode 100644 index 0000000000..8f1f5d6017 --- /dev/null +++ b/tests/corpus/7124-webcrypto-secret-keys.ts @@ -0,0 +1,91 @@ +// crypto.crypto.subtle.generateKey, exportKey and importKey for secret keys: +// HMAC and AES keys generated at every length, exported as raw bytes and +// as JSON Web Keys, imported back from both forms, and PBKDF2 and HKDF +// base keys imported from raw bytes. Random key bytes are checked by +// length and round trip, not by value. +const encoder = new TextEncoder(); + +type Key = Awaited>; + +function describe(key: Key): string { + return JSON.stringify({ + type: key.type, + extractable: key.extractable, + algorithm: key.algorithm, + usages: key.usages, + }); +} + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +async function show(label: string, run: () => Promise): Promise { + try { + console.log(label, await run()); + } catch (e) { + const error = e as Error & { code?: string }; + console.log(label, "rejects", error.name, error.code, error.message); + } +} + +for (const hash of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) { + await show(`hmac ${hash}`, async () => { + const key = await crypto.subtle.generateKey({ name: "HMAC", hash }, true, ["sign", "verify"]); + const raw = await crypto.subtle.exportKey("raw", key); + return `${describe(key)} ${raw.byteLength}`; + }); +} +await show("hmac length 100", async () => { + const key = await crypto.subtle.generateKey({ name: "HMAC", hash: { name: "SHA-512" }, length: 100 }, true, ["sign"]); + const raw = new Uint8Array(await crypto.subtle.exportKey("raw", key)); + return `${describe(key)} ${raw.length} ${raw[12]! & 0x0f}`; +}); +for (const name of ["AES-GCM", "AES-CBC", "AES-CTR", "AES-KW"]) { + for (const length of [128, 192, 256]) { + await show(`${name} ${length}`, async () => { + const usages: ("wrapKey" | "unwrapKey" | "encrypt" | "decrypt")[] = + name === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"]; + const key = await crypto.subtle.generateKey({ name, length }, true, usages); + const jwk = await crypto.subtle.exportKey("jwk", key); + return `${describe(key)} ${jwk.alg} ${jwk.kty} ${jwk.k!.length} ${JSON.stringify(jwk.key_ops)} ${jwk.ext}`; + }); + } +} + +// Fixed key bytes: the exported forms are deterministic. +const bytes = new Uint8Array(32); +for (let i = 0; i < bytes.length; i++) bytes[i] = i * 7 + 1; +const hmac = await crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-384" }, true, ["verify", "sign"]); +const hmacJwk = await crypto.subtle.exportKey("jwk", hmac); +console.log(JSON.stringify(hmacJwk), Object.keys(hmacJwk).join(",")); +console.log(hex(await crypto.subtle.exportKey("raw", hmac)) === Buffer.from(bytes).toString("hex")); +const reimported = await crypto.subtle.importKey("jwk", hmacJwk, { name: "HMAC", hash: "SHA-384" }, true, ["sign"]); +console.log(describe(reimported)); +const signature = await crypto.subtle.sign("HMAC", reimported, encoder.encode("payload")); +console.log(await crypto.subtle.verify("HMAC", hmac, signature, encoder.encode("payload"))); + +const short = await crypto.subtle.importKey("raw", bytes.subarray(0, 20), { name: "HMAC", hash: "SHA-1", length: 156 }, true, ["sign"]); +console.log(describe(short), JSON.stringify(await crypto.subtle.exportKey("jwk", short))); +console.log(hex(await crypto.subtle.exportKey("raw-secret", short))); + +for (const length of [16, 24, 32]) { + const key = await crypto.subtle.importKey("raw", bytes.subarray(0, length), "AES-GCM", true, ["encrypt"]); + const jwk = await crypto.subtle.exportKey("jwk", key); + console.log(describe(key), JSON.stringify(jwk)); + const back = await crypto.subtle.importKey("jwk", jwk, { name: "AES-GCM" }, false, ["encrypt"]); + console.log(describe(back)); +} +const cbc = await crypto.subtle.importKey("raw-secret", bytes.subarray(0, 16), "aes-cbc", true, ["decrypt", "unwrapKey"]); +console.log(describe(cbc), hex(await crypto.subtle.exportKey("raw", cbc))); +const wrap = await crypto.subtle.importKey("raw", bytes.subarray(0, 24), "AES-KW", true, ["unwrapKey", "wrapKey"]); +console.log(describe(wrap), JSON.stringify(await crypto.subtle.exportKey("jwk", wrap))); + +const password = encoder.encode("correct horse battery staple"); +const pbkdf2 = await crypto.subtle.importKey("raw", password, "PBKDF2", false, ["deriveBits", "deriveKey"]); +console.log(describe(pbkdf2)); +const hkdf = await crypto.subtle.importKey("raw-secret", bytes, { name: "hkdf" }, false, ["deriveKey"]); +console.log(describe(hkdf)); +const empty = await crypto.subtle.importKey("raw", new Uint8Array(0), "PBKDF2", false, ["deriveBits"]); +console.log(describe(empty)); +console.log(hmac instanceof CryptoKey, pbkdf2 instanceof CryptoKey); diff --git a/tests/corpus/7125-webcrypto-key-errors.mjs b/tests/corpus/7125-webcrypto-key-errors.mjs new file mode 100644 index 0000000000..7f14ae0d74 --- /dev/null +++ b/tests/corpus/7125-webcrypto-key-errors.mjs @@ -0,0 +1,95 @@ +// Rejections of crypto.crypto.subtle.generateKey, exportKey and importKey for +// secret keys, in Node's order: argument conversion, algorithm +// normalization, usages, then the per-algorithm checks, and the JSON Web +// Key validation of oct keys. + +async function attempt(label, run) { + try { + const value = await run(); + const shown = value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value instanceof CryptoKey ? JSON.stringify(value.algorithm) : JSON.stringify(value); + console.log(label, "resolves", shown); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +await attempt("generate too few", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true)); +await attempt("generate unknown", () => crypto.subtle.generateKey({ name: "FOO" }, true, ["sign"])); +await attempt("generate PBKDF2", () => crypto.subtle.generateKey({ name: "PBKDF2" }, false, ["deriveBits"])); +await attempt("generate no name", () => crypto.subtle.generateKey({ hash: "SHA-1" }, true, ["sign"])); +await attempt("generate hmac string", () => crypto.subtle.generateKey("HMAC", true, ["sign"])); +await attempt("generate hmac no hash", () => crypto.subtle.generateKey({ name: "HMAC" }, true, ["sign"])); +await attempt("generate hmac md5", () => crypto.subtle.generateKey({ name: "HMAC", hash: "MD5" }, true, ["sign"])); +await attempt("generate hmac bogus hash", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-999" }, true, ["sign"])); +await attempt("generate hmac length 0", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1", length: 0 }, true, ["sign"])); +await attempt("generate hmac length NaN", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1", length: NaN }, true, ["sign"])); +await attempt("generate hmac length -1", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1", length: -1 }, true, ["sign"])); +await attempt("generate hmac encrypt", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, ["encrypt"])); +await attempt("generate hmac no usages", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, [])); +await attempt("generate bad usage", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, ["fly"])); +await attempt("generate usages string", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, "sign")); +await attempt("generate aes no length", () => crypto.subtle.generateKey({ name: "AES-GCM" }, true, ["encrypt"])); +await attempt("generate aes length 100", () => crypto.subtle.generateKey({ name: "AES-GCM", length: 100 }, true, ["encrypt"])); +await attempt("generate aes length 70000", () => crypto.subtle.generateKey({ name: "AES-CBC", length: 70000 }, true, ["encrypt"])); +await attempt("generate aes sign", () => crypto.subtle.generateKey({ name: "AES-CTR", length: 128 }, true, ["sign"])); +await attempt("generate aes-kw encrypt", () => crypto.subtle.generateKey({ name: "AES-KW", length: 128 }, true, ["encrypt"])); +await attempt("generate aes no usages", () => crypto.subtle.generateKey({ name: "AES-GCM", length: 128 }, true, [])); +await attempt("generate aes string length", async () => (await crypto.subtle.generateKey({ name: "aes-gcm", length: "256" }, true, ["encrypt"])).algorithm); + +const hmac = await crypto.subtle.importKey("raw", new Uint8Array(20), { name: "HMAC", hash: "SHA-256" }, true, ["sign"]); +const locked = await crypto.subtle.importKey("raw", new Uint8Array(16), "AES-GCM", false, ["encrypt"]); +const pbkdf2 = await crypto.subtle.importKey("raw", new Uint8Array(4), "PBKDF2", false, ["deriveBits"]); +await attempt("export too few", () => crypto.subtle.exportKey("raw")); +await attempt("export bad format", () => crypto.subtle.exportKey("pem", hmac)); +await attempt("export not a key", () => crypto.subtle.exportKey("raw", { type: "secret" })); +await attempt("export spki", () => crypto.subtle.exportKey("spki", hmac)); +await attempt("export pkcs8", () => crypto.subtle.exportKey("pkcs8", hmac)); +await attempt("export raw-public", () => crypto.subtle.exportKey("raw-public", hmac)); +await attempt("export non-extractable", () => crypto.subtle.exportKey("raw", locked)); +await attempt("export pbkdf2", () => crypto.subtle.exportKey("raw", pbkdf2)); +await attempt("export pbkdf2 jwk", () => crypto.subtle.exportKey("jwk", pbkdf2)); + +const sixteen = new Uint8Array(16); +await attempt("import aes 15 bytes", () => crypto.subtle.importKey("raw", new Uint8Array(15), "AES-GCM", true, ["encrypt"])); +await attempt("import aes sign", () => crypto.subtle.importKey("raw", sixteen, "AES-GCM", true, ["sign"])); +await attempt("import aes-kw encrypt", () => crypto.subtle.importKey("raw", sixteen, "AES-KW", true, ["encrypt"])); +await attempt("import aes no usages", () => crypto.subtle.importKey("raw", sixteen, "AES-CBC", true, [])); +await attempt("import aes spki", () => crypto.subtle.importKey("spki", sixteen, "AES-CBC", true, ["encrypt"])); +await attempt("import aes length ignored", () => crypto.subtle.importKey("raw", sixteen, { name: "AES-CTR", length: 256 }, true, ["encrypt"])); +await attempt("import pbkdf2 extractable", () => crypto.subtle.importKey("raw", sixteen, "PBKDF2", true, ["deriveBits"])); +await attempt("import pbkdf2 sign", () => crypto.subtle.importKey("raw", sixteen, "PBKDF2", false, ["sign"])); +await attempt("import pbkdf2 no usages", () => crypto.subtle.importKey("raw", sixteen, "PBKDF2", false, [])); +await attempt("import hkdf jwk", () => crypto.subtle.importKey("jwk", { kty: "oct", k: "AAAA" }, "HKDF", false, ["deriveKey"])); +await attempt("import hkdf pkcs8", () => crypto.subtle.importKey("pkcs8", sixteen, "HKDF", false, ["deriveKey"])); +await attempt("sign with aes key", () => crypto.subtle.sign("HMAC", locked, sixteen)); + +const hs256 = { name: "HMAC", hash: "SHA-256" }; +const jwk = (data, alg = hs256, extractable = true, usages = ["sign"]) => crypto.subtle.importKey("jwk", data, alg, extractable, usages); +await attempt("jwk ok", () => jwk({ kty: "oct", k: "AAECAwQFBgc" })); +await attempt("jwk alg match", () => jwk({ kty: "oct", k: "AAECAwQFBgc", alg: "HS256" })); +await attempt("jwk alg mismatch", () => jwk({ kty: "oct", k: "AAECAwQFBgc", alg: "HS512" })); +await attempt("jwk no kty", () => jwk({ k: "AAECAwQFBgc" })); +await attempt("jwk kty RSA", () => jwk({ kty: "RSA", k: "AAECAwQFBgc" })); +await attempt("jwk no k", () => jwk({ kty: "oct" })); +await attempt("jwk k number", () => jwk({ kty: "oct", k: 5 })); +await attempt("jwk k empty", () => jwk({ kty: "oct", k: "" })); +await attempt("jwk k standard alphabet", () => jwk({ kty: "oct", k: "AA+/AA==" })); +await attempt("jwk k junk", () => jwk({ kty: "oct", k: "!!!!" })); +await attempt("jwk use enc", () => jwk({ kty: "oct", k: "AAECAwQFBgc", use: "enc" })); +await attempt("jwk use sig", () => jwk({ kty: "oct", k: "AAECAwQFBgc", use: "sig" })); +await attempt("jwk key_ops superset", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: ["sign", "verify", "fly"] })); +await attempt("jwk key_ops missing", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: ["verify"] })); +await attempt("jwk key_ops duplicate", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: ["sign", "sign"] })); +await attempt("jwk key_ops string", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: "sign" })); +await attempt("jwk ext false", () => jwk({ kty: "oct", k: "AAECAwQFBgc", ext: false })); +await attempt("jwk ext false locked", () => jwk({ kty: "oct", k: "AAECAwQFBgc", ext: false }, hs256, false)); +await attempt("jwk not an object", () => jwk("key")); +await attempt("jwk null", () => jwk(null)); +await attempt("jwk kty symbol", () => jwk({ kty: Symbol("oct"), k: "AA" })); +await attempt("jwk hmac length", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, { name: "HMAC", hash: "SHA-1", length: 60 })); +await attempt("jwk hmac length mismatch", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, { name: "HMAC", hash: "SHA-1", length: 128 })); +await attempt("jwk no usages", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, hs256, true, [])); +await attempt("jwk aes", () => jwk({ kty: "oct", k: "AAECAwQFBgcICQoLDA0ODw", alg: "A128GCM" }, "AES-GCM", true, ["encrypt"])); +await attempt("jwk aes alg mismatch", () => jwk({ kty: "oct", k: "AAECAwQFBgcICQoLDA0ODw", alg: "A256GCM" }, "AES-GCM", true, ["encrypt"])); +await attempt("jwk aes use sig", () => jwk({ kty: "oct", k: "AAECAwQFBgcICQoLDA0ODw", use: "sig" }, "AES-GCM", true, ["encrypt"])); +await attempt("jwk aes length", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, "AES-CBC", true, ["encrypt"])); diff --git a/tests/corpus/7126-webcrypto-aes.ts b/tests/corpus/7126-webcrypto-aes.ts new file mode 100644 index 0000000000..0e7be75485 --- /dev/null +++ b/tests/corpus/7126-webcrypto-aes.ts @@ -0,0 +1,77 @@ +// crypto.crypto.subtle.encrypt and decrypt with AES-GCM, AES-CBC and AES-CTR: +// fixed keys, IVs and inputs give Node's exact ciphertexts, every key +// length and GCM tag length round-trips, and CTR counters wrap within +// their low `length` bits. +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +function bytes(length: number, seed: number): Uint8Array { + const out = new Uint8Array(length); + for (let i = 0; i < length; i++) out[i] = (i * 31 + seed) & 0xff; + return out; +} + +const plaintext = encoder.encode("The quick brown fox jumps over the lazy dog"); +for (const length of [16, 24, 32]) { + const raw = bytes(length, length); + const gcm = await crypto.subtle.importKey("raw", raw, "AES-GCM", false, ["encrypt", "decrypt"]); + const cbc = await crypto.subtle.importKey("raw", raw, "AES-CBC", false, ["encrypt", "decrypt"]); + const ctr = await crypto.subtle.importKey("raw", raw, "AES-CTR", false, ["encrypt", "decrypt"]); + const iv = bytes(12, 7); + const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, gcm, plaintext); + console.log("gcm", length, sealed.byteLength, hex(sealed)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, gcm, sealed))); + const aad = encoder.encode("header"); + const tagged = await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData: aad, tagLength: 96 }, gcm, plaintext); + console.log("gcm aad", length, hex(tagged)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv, additionalData: aad, tagLength: 96 }, gcm, tagged))); + const cbcIv = bytes(16, 3); + const padded = await crypto.subtle.encrypt({ name: "AES-CBC", iv: cbcIv }, cbc, plaintext); + console.log("cbc", length, padded.byteLength, hex(padded)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-CBC", iv: cbcIv }, cbc, padded))); + const counter = bytes(16, 9); + const streamed = await crypto.subtle.encrypt({ name: "AES-CTR", counter, length: 64 }, ctr, plaintext); + console.log("ctr", length, hex(streamed)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-CTR", counter, length: 64 }, ctr, streamed))); +} + +const key = await crypto.subtle.importKey("raw", bytes(16, 1), "AES-GCM", true, ["encrypt", "decrypt"]); +for (const tagLength of [32, 64, 96, 104, 112, 120, 128]) { + const iv = bytes(16, tagLength); + const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv, tagLength }, key, plaintext.subarray(0, 5)); + const opened = await crypto.subtle.decrypt({ name: "AES-GCM", iv, tagLength }, key, sealed); + console.log("tag", tagLength, sealed.byteLength, hex(sealed), decoder.decode(opened)); +} +console.log("gcm empty", hex(await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(0)))); +console.log("gcm long iv", hex(await crypto.subtle.encrypt({ name: "AES-GCM", iv: bytes(128, 5) }, key, plaintext.subarray(0, 3)))); + +const cbcKey = await crypto.subtle.importKey("raw", bytes(16, 2), "AES-CBC", false, ["encrypt", "decrypt"]); +for (const length of [0, 1, 15, 16, 17, 32]) { + const sealed = await crypto.subtle.encrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, cbcKey, bytes(length, 4)); + const opened = await crypto.subtle.decrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, cbcKey, sealed); + console.log("cbc length", length, sealed.byteLength, opened.byteLength, hex(opened) === hex(bytes(length, 4).buffer as ArrayBuffer)); +} + +const ctrKey = await crypto.subtle.importKey("raw", new Uint8Array(16), "AES-CTR", false, ["encrypt"]); +const allOnes = new Uint8Array(16).fill(0xff); +const zeros = new Uint8Array(64); +console.log("ctr wrap 4", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: allOnes, length: 4 }, ctrKey, zeros))); +console.log("ctr wrap 128", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: allOnes, length: 128 }, ctrKey, zeros))); +console.log("ctr wrap 2", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: allOnes, length: 2 }, ctrKey, zeros))); +console.log("ctr 1 bit, 2 blocks", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: new Uint8Array(16), length: 1 }, ctrKey, zeros.subarray(0, 32)))); +console.log("ctr partial", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: bytes(16, 6), length: 128 }, ctrKey, zeros.subarray(0, 21)))); + +const generated = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); +const iv = crypto.getRandomValues(new Uint8Array(12)); +const message = encoder.encode("generated key round trip"); +const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, generated, message); +console.log(sealed.byteLength, decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, generated, sealed))); +const exported = await crypto.subtle.exportKey("jwk", generated); +const reimported = await crypto.subtle.importKey("jwk", exported, "AES-GCM", false, ["decrypt"]); +console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, reimported, sealed))); +console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, generated, new DataView(sealed)))); +console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv.buffer as ArrayBuffer }, generated, Buffer.from(sealed)))); diff --git a/tests/corpus/7127-webcrypto-aes-errors.mjs b/tests/corpus/7127-webcrypto-aes-errors.mjs new file mode 100644 index 0000000000..7ce51230a7 --- /dev/null +++ b/tests/corpus/7127-webcrypto-aes-errors.mjs @@ -0,0 +1,71 @@ +// Rejections of crypto.crypto.subtle.encrypt and decrypt in Node's order: +// argument conversion, the AesCbcParams, AesCtrParams and AeadParams +// dictionaries, the key checks, then the cipher's own OperationErrors +// (bad tags, bad padding, IV and counter limits). + +async function attempt(label, run) { + try { + const value = await run(); + console.log(label, "resolves", value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +const raw = new Uint8Array(16).map((_, i) => i); +const gcm = await crypto.subtle.importKey("raw", raw, "AES-GCM", true, ["encrypt", "decrypt"]); +const cbc = await crypto.subtle.importKey("raw", raw, "AES-CBC", true, ["encrypt", "decrypt"]); +const ctr = await crypto.subtle.importKey("raw", raw, "AES-CTR", true, ["encrypt", "decrypt"]); +const encryptOnly = await crypto.subtle.importKey("raw", raw, "AES-GCM", true, ["encrypt"]); +const hmac = await crypto.subtle.importKey("raw", raw, { name: "HMAC", hash: "SHA-1" }, true, ["sign"]); +const iv12 = new Uint8Array(12); +const iv16 = new Uint8Array(16); +const data = new TextEncoder().encode("hello world"); + +await attempt("too few", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, gcm)); +await attempt("decrypt too few", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 })); +await attempt("not a key", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, JSON.parse("{}"), data)); +await attempt("data string", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, gcm, "hello")); +await attempt("unknown algorithm", () => crypto.subtle.encrypt({ name: "AES-XTS" }, gcm, data)); +await attempt("AES-KW", () => crypto.subtle.encrypt({ name: "AES-KW" }, gcm, data)); +await attempt("no name", () => crypto.subtle.encrypt({ iv: iv12 }, gcm, data)); +await attempt("string algorithm", () => crypto.subtle.encrypt("AES-GCM", gcm, data)); +await attempt("gcm no iv", () => crypto.subtle.encrypt({ name: "AES-GCM" }, gcm, data)); +await attempt("gcm iv string", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: "abc" }, gcm, data)); +await attempt("gcm aad string", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, additionalData: "x" }, gcm, data)); +await attempt("gcm aad null", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, additionalData: null }, gcm, data)); +await attempt("gcm tag 100", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, tagLength: 100 }, gcm, data)); +await attempt("gcm tag 300", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, tagLength: 300 }, gcm, data)); +await attempt("gcm tag NaN", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, tagLength: NaN }, gcm, data)); +await attempt("gcm tag string", () => crypto.subtle.encrypt({ name: "aes-gcm", iv: iv12, tagLength: "96" }, gcm, data)); +await attempt("gcm iv 11 bytes", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(11) }, gcm, data)); +await attempt("gcm iv 129 bytes", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(129) }, gcm, data)); +await attempt("gcm iv empty", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(0) }, gcm, data)); +const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, gcm, data)); +const tampered = sealed.slice(); +tampered[0] ^= 1; +await attempt("gcm tampered", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, gcm, tampered)); +await attempt("gcm wrong aad", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12, additionalData: data }, gcm, sealed)); +await attempt("gcm wrong tag length", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12, tagLength: 96 }, gcm, sealed)); +await attempt("gcm shorter than tag", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, gcm, new Uint8Array(15))); +await attempt("gcm tag only", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, gcm, new Uint8Array(16))); +await attempt("cbc no iv", () => crypto.subtle.encrypt({ name: "AES-CBC" }, cbc, data)); +await attempt("cbc iv 12 bytes", () => crypto.subtle.encrypt({ name: "AES-CBC", iv: iv12 }, cbc, data)); +await attempt("cbc decrypt 15 bytes", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: iv16 }, cbc, new Uint8Array(15))); +await attempt("cbc decrypt empty", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: iv16 }, cbc, new Uint8Array(0))); +const block = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-CBC", iv: iv16 }, cbc, data)); +const badPad = block.slice(); +badPad[15] ^= 0x55; +await attempt("cbc bad padding", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: iv16 }, cbc, badPad)); +await attempt("cbc other iv", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: new Uint8Array(16).fill(1) }, cbc, block)); +await attempt("ctr no counter", () => crypto.subtle.encrypt({ name: "AES-CTR", length: 64 }, ctr, data)); +await attempt("ctr no length", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16 }, ctr, data)); +await attempt("ctr length 0", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 0 }, ctr, data)); +await attempt("ctr length 129", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 129 }, ctr, data)); +await attempt("ctr length 300", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 300 }, ctr, data)); +await attempt("ctr counter 8 bytes", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: new Uint8Array(8), length: 64 }, ctr, data)); +await attempt("ctr 1 bit, 3 blocks", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 1 }, ctr, new Uint8Array(48))); +await attempt("ctr empty", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 1 }, ctr, new Uint8Array(0))); +await attempt("algorithm mismatch", () => crypto.subtle.encrypt({ name: "AES-CBC", iv: iv16 }, gcm, data)); +await attempt("hmac key", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, hmac, data)); +await attempt("missing usage", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, encryptOnly, sealed)); diff --git a/tests/corpus/7128-webcrypto-derive.ts b/tests/corpus/7128-webcrypto-derive.ts new file mode 100644 index 0000000000..4badedb669 --- /dev/null +++ b/tests/corpus/7128-webcrypto-derive.ts @@ -0,0 +1,45 @@ +// crypto.crypto.subtle.deriveBits and deriveKey with PBKDF2 and HKDF: Node's +// exact derived bytes for every SHA digest, zero-length derivations, the +// SCRAM-style PBKDF2-then-HMAC chain, and derived AES and HMAC keys that +// encrypt and sign. +const encoder = new TextEncoder(); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +const password = await crypto.subtle.importKey("raw", encoder.encode("pencil"), "PBKDF2", false, ["deriveBits", "deriveKey"]); +const ikm = await crypto.subtle.importKey("raw", encoder.encode("input keying material"), { name: "HKDF" }, false, ["deriveBits", "deriveKey"]); +const salt = encoder.encode("QSXCR+Q6sek8bf92"); +const info = encoder.encode("context"); + +for (const hash of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) { + console.log("pbkdf2", hash, hex(await crypto.subtle.deriveBits({ name: "PBKDF2", hash, salt, iterations: 4096 }, password, 256))); + console.log("hkdf", hash, hex(await crypto.subtle.deriveBits({ name: "HKDF", hash: { name: hash }, salt, info }, ikm, 256))); +} +console.log("pbkdf2 0", (await crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 1 }, password, 0)).byteLength); +console.log("hkdf 0", (await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt, info }, ikm, 0)).byteLength); +console.log("hkdf empty salt and info", hex(await crypto.subtle.deriveBits({ name: "hkdf", hash: "SHA-256", salt: new Uint8Array(0), info: new Uint8Array(0) }, ikm, 128))); +console.log("hkdf max", (await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-1", salt, info }, ikm, 255 * 20 * 8)).byteLength); + +// SCRAM-SHA-256's client proof inputs, the way database drivers compute them. +const saltedPassword = await crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 4096 }, password, 256); +const salted = await crypto.subtle.importKey("raw", saltedPassword, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +const clientKey = await crypto.subtle.sign("HMAC", salted, encoder.encode("Client Key")); +console.log("client key", hex(clientKey)); +console.log("stored key", hex(await crypto.subtle.digest("SHA-256", clientKey))); + +const aes = await crypto.subtle.deriveKey({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 1000 }, password, { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); +console.log(JSON.stringify(aes.algorithm), aes.usages.join(","), aes.extractable); +console.log("aes key", hex(await crypto.subtle.exportKey("raw", aes))); +const iv = new Uint8Array(12); +const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aes, encoder.encode("derived")); +console.log(hex(sealed), new TextDecoder().decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, aes, sealed))); + +const mac = await crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt, info }, ikm, { name: "HMAC", hash: "SHA-384" }, false, ["sign", "verify"]); +console.log(JSON.stringify(mac.algorithm), mac.extractable); +console.log("mac", hex(await crypto.subtle.sign("HMAC", mac, encoder.encode("message")))); +const shortMac = await crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-1", salt, info }, ikm, { name: "HMAC", hash: "SHA-1", length: 128 }, true, ["sign"]); +console.log(JSON.stringify(shortMac.algorithm), hex(await crypto.subtle.exportKey("raw", shortMac))); +const wrap = await crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-512", salt, info }, ikm, { name: "AES-KW", length: 128 }, true, ["wrapKey"]); +console.log(JSON.stringify(wrap.algorithm), JSON.stringify(await crypto.subtle.exportKey("jwk", wrap))); diff --git a/tests/corpus/7129-webcrypto-derive-errors.mjs b/tests/corpus/7129-webcrypto-derive-errors.mjs new file mode 100644 index 0000000000..83149b61b1 --- /dev/null +++ b/tests/corpus/7129-webcrypto-derive-errors.mjs @@ -0,0 +1,72 @@ +// Rejections of crypto.crypto.subtle.deriveBits and deriveKey in Node's order: +// argument conversion, the Pbkdf2Params and HkdfParams dictionaries, the +// base key's usages and algorithm, the length rules, and for deriveKey +// the derived key type's normalization and import. +const encoder = new TextEncoder(); + +async function attempt(label, run) { + try { + const value = await run(); + const shown = value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value instanceof CryptoKey ? JSON.stringify(value.algorithm) : value; + console.log(label, "resolves", shown); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +const password = await crypto.subtle.importKey("raw", encoder.encode("password"), "PBKDF2", false, ["deriveBits", "deriveKey"]); +const ikm = await crypto.subtle.importKey("raw", encoder.encode("input key"), "HKDF", false, ["deriveBits", "deriveKey"]); +const bitsOnly = await crypto.subtle.importKey("raw", encoder.encode("x"), "PBKDF2", false, ["deriveBits"]); +const keyOnly = await crypto.subtle.importKey("raw", encoder.encode("x"), "PBKDF2", false, ["deriveKey"]); +const hmac = await crypto.subtle.importKey("raw", encoder.encode("x"), { name: "HMAC", hash: "SHA-1" }, false, ["sign"]); +const salt = encoder.encode("salt"); +const pbkdf2 = (extra = {}) => ({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 1000, ...extra }); +const hkdf = (extra = {}) => ({ name: "HKDF", hash: "SHA-256", salt, info: encoder.encode("info"), ...extra }); + +await attempt("bits too few", () => crypto.subtle.deriveBits(pbkdf2())); +await attempt("bits not a key", () => crypto.subtle.deriveBits(pbkdf2(), JSON.parse("{}"), 64)); +await attempt("bits length null", () => crypto.subtle.deriveBits(pbkdf2(), password, null)); +await attempt("bits length undefined", () => crypto.subtle.deriveBits(pbkdf2(), password, undefined)); +await attempt("bits length absent", () => crypto.subtle.deriveBits(pbkdf2(), password)); +await attempt("bits length 12", () => crypto.subtle.deriveBits(pbkdf2(), password, 12)); +await attempt("bits length -1", () => crypto.subtle.deriveBits(pbkdf2(), password, -1)); +await attempt("bits length NaN", () => crypto.subtle.deriveBits(pbkdf2(), password, NaN)); +await attempt("bits length string", () => crypto.subtle.deriveBits(pbkdf2(), password, "64")); +await attempt("bits unknown", () => crypto.subtle.deriveBits({ name: "SCRYPT" }, password, 64)); +await attempt("bits no name", () => crypto.subtle.deriveBits({ hash: "SHA-1" }, password, 64)); +await attempt("pbkdf2 iterations 0", () => crypto.subtle.deriveBits(pbkdf2({ iterations: 0 }), password, 64)); +await attempt("pbkdf2 iterations 2^31", () => crypto.subtle.deriveBits(pbkdf2({ iterations: 2 ** 31 }), password, 64)); +await attempt("pbkdf2 iterations -1", () => crypto.subtle.deriveBits(pbkdf2({ iterations: -1 }), password, 64)); +await attempt("pbkdf2 no iterations", () => crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt }, password, 64)); +await attempt("pbkdf2 no salt", () => crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", iterations: 1 }, password, 64)); +await attempt("pbkdf2 no hash", () => crypto.subtle.deriveBits({ name: "PBKDF2", salt, iterations: 1 }, password, 64)); +await attempt("pbkdf2 string", () => crypto.subtle.deriveBits("PBKDF2", password, 64)); +await attempt("pbkdf2 md5", () => crypto.subtle.deriveBits(pbkdf2({ hash: "MD5" }), password, 64)); +await attempt("pbkdf2 bogus sha", () => crypto.subtle.deriveBits(pbkdf2({ hash: "SHA-999" }), password, 64)); +await attempt("pbkdf2 salt string", () => crypto.subtle.deriveBits(pbkdf2({ salt: "salt" }), password, 64)); +await attempt("pbkdf2 hkdf key", () => crypto.subtle.deriveBits(pbkdf2(), ikm, 64)); +await attempt("pbkdf2 hmac key", () => crypto.subtle.deriveBits(pbkdf2(), hmac, 64)); +await attempt("pbkdf2 no deriveBits usage", () => crypto.subtle.deriveBits(pbkdf2(), keyOnly, 64)); +await attempt("hkdf no info", () => crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt }, ikm, 64)); +await attempt("hkdf no salt", () => crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", info: salt }, ikm, 64)); +await attempt("hkdf info 1025 bytes", () => crypto.subtle.deriveBits(hkdf({ info: new Uint8Array(1025) }), ikm, 64)); +await attempt("hkdf over maximum", () => crypto.subtle.deriveBits(hkdf({ hash: "SHA-1" }), ikm, 255 * 20 * 8 + 8)); +await attempt("hkdf length 4", () => crypto.subtle.deriveBits(hkdf(), ikm, 4)); +await attempt("hkdf null", () => crypto.subtle.deriveBits(hkdf(), ikm, null)); + +const aes = { name: "AES-GCM", length: 128 }; +await attempt("key too few", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, true)); +await attempt("key not a key", () => crypto.subtle.deriveKey(pbkdf2(), "key", aes, true, ["encrypt"])); +await attempt("key no deriveKey usage", () => crypto.subtle.deriveKey(pbkdf2(), bitsOnly, aes, true, ["encrypt"])); +await attempt("key mismatch", () => crypto.subtle.deriveKey(hkdf(), password, aes, true, ["encrypt"])); +await attempt("key aes no length", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "AES-GCM" }, true, ["encrypt"])); +await attempt("key aes length 100", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "AES-CBC", length: 100 }, true, ["encrypt"])); +await attempt("key aes sign usage", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, true, ["sign"])); +await attempt("key aes no usages", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, true, [])); +await attempt("key hmac no hash", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "HMAC" }, true, ["sign"])); +await attempt("key hmac length 0", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "HMAC", hash: "SHA-1", length: 0 }, true, ["sign"])); +await attempt("key hmac length 100", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "HMAC", hash: "SHA-1", length: 100 }, true, ["sign"])); +await attempt("key hkdf type", () => crypto.subtle.deriveKey(pbkdf2(), password, "HKDF", false, ["deriveBits"])); +await attempt("key unknown type", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "TWOFISH" }, true, ["encrypt"])); +await attempt("key pbkdf2 type", () => crypto.subtle.deriveKey(hkdf(), ikm, { name: "PBKDF2" }, false, ["deriveKey"])); +await attempt("key ok", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, false, ["encrypt"])); diff --git a/tests/diagnostics/webcrypto-refusals.ts b/tests/diagnostics/webcrypto-refusals.ts index d162872fbc..309e70c4e3 100644 --- a/tests/diagnostics/webcrypto-refusals.ts +++ b/tests/diagnostics/webcrypto-refusals.ts @@ -1,17 +1,8 @@ -// The native WebCrypto surface is HMAC importKey('raw'), sign and verify, -// and the SHA-1/SHA-2 digests, called directly on crypto.subtle. The 'jwk' -// key format and crypto.subtle or its methods taken as values are refused -// by name; the bundled declarations omit the other SubtleCrypto methods -// (node-types/fenced.ts pins their fence under @types/node). -const fromJwk = await crypto.subtle.importKey( - "jwk", - { kty: "oct", k: "c2VjcmV0" }, - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign"], -); -console.log(fromJwk.type); - +// The native WebCrypto surface is crypto.subtle's secret-key slice, called +// directly on crypto.subtle. crypto.subtle or its methods taken as values +// are refused by name; the bundled declarations omit the other +// SubtleCrypto methods (node-types/fenced.ts pins their fence under +// @types/node). const subtle = crypto.subtle; console.log(typeof subtle); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 16a74081fa..dfd07d446f 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -103,13 +103,13 @@ cipherStream.on("data", () => {}); import { createECDH } from "node:crypto"; createECDH("sect163k1"); createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); -/* WebCrypto beyond the HMAC slice: @types/node declares the whole - * SubtleCrypto surface, and the methods outside importKey, sign, verify - * and digest fence by name. */ -async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); +/* WebCrypto beyond the secret-key slice: @types/node declares the whole + * SubtleCrypto surface, and the methods outside the slice, such as + * wrapKey, fence by name. */ +async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); } -void encryptWithWebCrypto; +void wrapWithWebCrypto; /* Node's navigator global: typeof navigator and navigator.userAgent * compile; the other members fence by name. */ const cores = navigator.hardwareConcurrency; diff --git a/tests/fixtures/node-types/webcrypto-keys.ts b/tests/fixtures/node-types/webcrypto-keys.ts new file mode 100644 index 0000000000..be16271d88 --- /dev/null +++ b/tests/fixtures/node-types/webcrypto-keys.ts @@ -0,0 +1,32 @@ +/* WebCrypto secret keys under @types/node: generateKey, exportKey and + * importKey for HMAC and AES keys, and PBKDF2 key imports, lower to the + * same native values as under the bundled declarations. @types/node + * names the dictionary, key and JSON Web Key types in node:crypto's + * webcrypto namespace. */ +import type { webcrypto } from "node:crypto"; + +async function main(): Promise { + const hmacParams: webcrypto.HmacKeyGenParams = { name: "HMAC", hash: "SHA-256", length: 256 }; + const hmac: webcrypto.CryptoKey = await crypto.subtle.generateKey(hmacParams, true, ["sign", "verify"]); + const hmacAlgorithm = hmac.algorithm as webcrypto.HmacKeyAlgorithm; + console.log(hmac.type, hmacAlgorithm.name, hmacAlgorithm.length, hmacAlgorithm.hash.name, hmac.usages.join(",")); + const raw: ArrayBuffer = await crypto.subtle.exportKey("raw", hmac); + console.log(raw.byteLength); + + const aesParams: webcrypto.AesKeyGenParams = { name: "AES-GCM", length: 192 }; + const aes = await crypto.subtle.generateKey(aesParams, true, ["encrypt", "decrypt"]); + const aesAlgorithm = aes.algorithm as webcrypto.AesKeyAlgorithm; + console.log(aesAlgorithm.name, aesAlgorithm.length, aes.extractable); + const jwk: webcrypto.JsonWebKey = await crypto.subtle.exportKey("jwk", aes); + console.log(jwk.kty, jwk.alg, jwk.ext, jwk.key_ops?.join(","), jwk.k?.length); + const back = await crypto.subtle.importKey("jwk", jwk, "AES-GCM", false, ["decrypt"]); + console.log(back.algorithm.name, back.extractable, back.usages.join(",")); + + const fixed = new Uint8Array(16).fill(9); + const imported = await crypto.subtle.importKey("raw", fixed, { name: "AES-CBC" }, true, ["encrypt"]); + console.log(Buffer.from(await crypto.subtle.exportKey("raw", imported)).toString("hex")); + const password = await crypto.subtle.importKey("raw", new TextEncoder().encode("pw"), "PBKDF2", false, ["deriveBits"]); + console.log(password.algorithm.name, password.extractable, password.usages.join(",")); +} + +void main(); diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 0bd13963d4..2f1f8cb2e0 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -228,18 +228,18 @@ fenced.ts:105:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer 104 | createECDH("sect163k1"); 105 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 106 | /* WebCrypto beyond the HMAC slice: @types/node declares the whole + 106 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole hint: Node reads the second argument as the key's input encoding and answers a Buffer; pass the output encoding third: computeSecret(key, undefined, encoding) -fenced.ts:110:9 - error SC2020: 'crypto.subtle.encrypt' is part of the standard library types but has no scriptc lowering yet +fenced.ts:110:9 - error SC2020: 'crypto.subtle.wrapKey' is part of the standard library types but has no scriptc lowering yet - 109 | async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - 110 | await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); + 109 | async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + 110 | await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); | ^~~~~~~~~~~~~~~~~~~~~ 111 | } - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle fenced.ts:115:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet diff --git a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt index 521edff3ce..e46099f699 100644 --- a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt +++ b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt @@ -1,26 +1,17 @@ -webcrypto-refusals.ts:7:3 - error SC2020: 'crypto.subtle.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet +webcrypto-refusals.ts:6:16 - error SC2020: 'crypto.subtle as a value' is part of the standard library types but has no scriptc lowering yet - 6 | const fromJwk = await crypto.subtle.importKey( - 7 | "jwk", - | ^~~~~ - 8 | { kty: "oct", k: "c2VjcmV0" }, + 5 | // @types/node). + 6 | const subtle = crypto.subtle; + | ^~~~~~~~~~~~~ + 7 | console.log(typeof subtle); - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle -webcrypto-refusals.ts:15:16 - error SC2020: 'crypto.subtle as a value' is part of the standard library types but has no scriptc lowering yet +webcrypto-refusals.ts:9:20 - error SC2020: 'crypto.subtle.sign as a value' is part of the standard library types but has no scriptc lowering yet - 14 | - 15 | const subtle = crypto.subtle; - | ^~~~~~~~~~~~~ - 16 | console.log(typeof subtle); - - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle - -webcrypto-refusals.ts:18:20 - error SC2020: 'crypto.subtle.sign as a value' is part of the standard library types but has no scriptc lowering yet - - 17 | - 18 | console.log(typeof crypto.subtle.sign); + 8 | + 9 | console.log(typeof crypto.subtle.sign); | ^~~~~~~~~~~~~~~~~~ - 19 | + 10 | - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file diff --git a/tests/harness/corpus-costs/7120-webcrypto-random-uuid.ts.json b/tests/harness/corpus-costs/7120-webcrypto-random-uuid.ts.json new file mode 100644 index 0000000000..ce52548ca2 --- /dev/null +++ b/tests/harness/corpus-costs/7120-webcrypto-random-uuid.ts.json @@ -0,0 +1 @@ +{ "plain": 4431, "san": 4365 } diff --git a/tests/harness/corpus-costs/7121-webcrypto-crypto-object.mjs.json b/tests/harness/corpus-costs/7121-webcrypto-crypto-object.mjs.json new file mode 100644 index 0000000000..db431c2ca5 --- /dev/null +++ b/tests/harness/corpus-costs/7121-webcrypto-crypto-object.mjs.json @@ -0,0 +1 @@ +{ "plain": 5043, "san": 4705 } diff --git a/tests/harness/corpus-costs/7122-webcrypto-node-crypto.ts.json b/tests/harness/corpus-costs/7122-webcrypto-node-crypto.ts.json new file mode 100644 index 0000000000..46469f2a92 --- /dev/null +++ b/tests/harness/corpus-costs/7122-webcrypto-node-crypto.ts.json @@ -0,0 +1 @@ +{ "plain": 4805, "san": 6923 } diff --git a/tests/harness/corpus-costs/7123-webcrypto-node-crypto.cjs.json b/tests/harness/corpus-costs/7123-webcrypto-node-crypto.cjs.json new file mode 100644 index 0000000000..682ba704a7 --- /dev/null +++ b/tests/harness/corpus-costs/7123-webcrypto-node-crypto.cjs.json @@ -0,0 +1 @@ +{ "plain": 4728, "san": 6725 } diff --git a/tests/harness/corpus-costs/7124-webcrypto-secret-keys.ts.json b/tests/harness/corpus-costs/7124-webcrypto-secret-keys.ts.json new file mode 100644 index 0000000000..73f1ce847c --- /dev/null +++ b/tests/harness/corpus-costs/7124-webcrypto-secret-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 5860, "san": 9241 } diff --git a/tests/harness/corpus-costs/7125-webcrypto-key-errors.mjs.json b/tests/harness/corpus-costs/7125-webcrypto-key-errors.mjs.json new file mode 100644 index 0000000000..ceff344ebb --- /dev/null +++ b/tests/harness/corpus-costs/7125-webcrypto-key-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 6582, "san": 9054 } diff --git a/tests/harness/corpus-costs/7126-webcrypto-aes.ts.json b/tests/harness/corpus-costs/7126-webcrypto-aes.ts.json new file mode 100644 index 0000000000..b1680985c9 --- /dev/null +++ b/tests/harness/corpus-costs/7126-webcrypto-aes.ts.json @@ -0,0 +1 @@ +{ "plain": 4563, "san": 6500 } diff --git a/tests/harness/corpus-costs/7127-webcrypto-aes-errors.mjs.json b/tests/harness/corpus-costs/7127-webcrypto-aes-errors.mjs.json new file mode 100644 index 0000000000..1ebb743b0a --- /dev/null +++ b/tests/harness/corpus-costs/7127-webcrypto-aes-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 4385, "san": 8939 } diff --git a/tests/harness/corpus-costs/7128-webcrypto-derive.ts.json b/tests/harness/corpus-costs/7128-webcrypto-derive.ts.json new file mode 100644 index 0000000000..2888d22eb8 --- /dev/null +++ b/tests/harness/corpus-costs/7128-webcrypto-derive.ts.json @@ -0,0 +1 @@ +{ "plain": 5042, "san": 9143 } diff --git a/tests/harness/corpus-costs/7129-webcrypto-derive-errors.mjs.json b/tests/harness/corpus-costs/7129-webcrypto-derive-errors.mjs.json new file mode 100644 index 0000000000..b2f70bf576 --- /dev/null +++ b/tests/harness/corpus-costs/7129-webcrypto-derive-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 5549, "san": 7441 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 8af6998ca6..00ee400436 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1634,14 +1634,14 @@ console.log(typeof promisePrototype, typeof promisePrototype.then, promiseProtot }); }); -// WebCrypto's native slice is HMAC importKey('raw'), sign and verify, and -// the SHA-1/SHA-2 digests. Algorithms Node recognizes outside that slice -// reject with a coded SC2020 Error instead of a Node-shaped success, and a -// JavaScript call with the 'jwk' format meets its named fence when it runs. +// WebCrypto's native slice is crypto.subtle's secret-key methods. +// Algorithms Node recognizes outside that slice reject with a coded SC2020 +// Error instead of a Node-shaped success, and a JavaScript call of a +// method outside it meets its named fence when it runs. // Node's own rejections (unknown names, bad usages) are corpus-pinned // (6292, 6294); these refusals have no Node twin, so they live here. describe("WebCrypto refusals (JS lane)", () => { - test("recognized algorithms outside the HMAC slice reject with SC2020", async () => { + test("recognized algorithms outside the secret-key slice reject with SC2020", async () => { const r = await compileAndRun( "webcrypto-unsupported-algorithms", `const bytes = new TextEncoder().encode("secret"); @@ -1662,10 +1662,10 @@ await attempt("sha3", () => crypto.subtle.digest("SHA3-256", bytes)); await attempt("sha3 hmac", () => crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA3-256" }, false, ["sign"]), ); -const format = JSON.parse('"jwk"'); -await attempt("computed jwk", () => - crypto.subtle.importKey(format, { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), -); +await attempt("ed25519 key", () => crypto.subtle.generateKey({ name: "Ed25519" }, false, ["sign"])); +const aes = await crypto.subtle.generateKey({ name: "AES-GCM", length: 128 }, false, ["encrypt"]); +await attempt("aes-ocb", () => crypto.subtle.encrypt({ name: "AES-OCB", iv: new Uint8Array(12) }, aes, bytes)); +await attempt("ecdh bits", () => crypto.subtle.deriveBits({ name: "ECDH" }, key, 256)); `, "mjs", ); @@ -1676,22 +1676,25 @@ await attempt("computed jwk", () => "ecdsa Error SC2020 crypto.subtle.sign with the ECDSA algorithm is not supported yet\n" + "sha3 Error SC2020 crypto.subtle.digest with the SHA3-256 algorithm is not supported yet\n" + "sha3 hmac Error SC2020 crypto.subtle.importKey with the SHA3-256 algorithm is not supported yet\n" + - "computed jwk Error SC2020 crypto.subtle.importKey with the 'jwk' key format is not supported yet\n", + "ed25519 key Error SC2020 crypto.subtle.generateKey with the Ed25519 algorithm is not supported yet\n" + + "aes-ocb Error SC2020 crypto.subtle.encrypt with the AES-OCB algorithm is not supported yet\n" + + "ecdh bits Error SC2020 crypto.subtle.deriveBits with the ECDH algorithm is not supported yet\n", ); }); - test("a literal 'jwk' import is a named fence where it runs", async () => { + test("a method outside the slice is a named fence where it runs", async () => { const r = await compileAndRun( - "webcrypto-jwk-literal", + "webcrypto-wrap-key", `console.log("before"); -await crypto.subtle.importKey("jwk", { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +const key = await crypto.subtle.generateKey({ name: "AES-KW", length: 128 }, true, ["wrapKey"]); +await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); `, "mjs", ); expect(r.exitCode).toBe(1); expect(r.stdout).toBe("before\n"); expect(r.stderr).toMatch( - /^Uncaught Error: 'crypto\.subtle\.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet.*\n$/, + /^Uncaught Error: 'crypto\.subtle\.wrapKey' is part of the standard library types but has no scriptc lowering yet.*\n$/, ); }); }); diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index dabb9b0bfc..8532ad6e90 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -1132,6 +1132,24 @@ test("node-types: WebCrypto HMAC matches Node under @types/node", async () => { expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: WebCrypto secret keys match Node under @types/node", async () => { + const outDir = outDirFor("node-webcrypto-keys"); + const entry = join(nodeTypesDir, "webcrypto-keys.ts"); + const result = await compile(entry, { + outPath: join(outDir, "webcrypto-keys"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: the navigator global's runtime probe matches Node under @types/node", async () => { const outDir = outDirFor("node-navigator"); const entry = join(nodeTypesDir, "navigator.ts"); From f00428ccb1aa6a7e0b3a384cb71b53bd4fe28217 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:53:03 -0500 Subject: [PATCH 082/395] Keep pipe() answering its destination and evaluate pipeline() arguments in source order --- .../frontend/lowering/builtins/hash-stream.ts | 16 +++--- .../src/frontend/lowering/lower-stream.ts | 52 +++++++++++++------ tests/corpus/7055-cipher-streams.ts | 40 ++++++++++++++ 3 files changed, 87 insertions(+), 21 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/builtins/hash-stream.ts b/packages/compiler/src/frontend/lowering/builtins/hash-stream.ts index f2f3625dd5..88f6c2d951 100644 --- a/packages/compiler/src/frontend/lowering/builtins/hash-stream.ts +++ b/packages/compiler/src/frontend/lowering/builtins/hash-stream.ts @@ -82,7 +82,10 @@ export function lowerHashStreamCall( if (!isValueStream(lowerer, access.expression)) return null; const info = lowerer.classes.get(TRANSFORM_CLASS); if (!info) return null; - return withValueStreamReceiver(lowerer, access.expression, locOf(call), () => + // pipe(dest) answers its destination, which may be a Transform too; + // the other members typed as the Transform answer `this` + const answersThis = name !== "pipe"; + return withValueStreamReceiver(lowerer, access.expression, locOf(call), answersThis, () => streamMember ? lowerStreamMethodCall(lowerer, call, access, info) : lowerEmitterMethodCall(lowerer, call, access, info), @@ -98,19 +101,20 @@ export function lowerValueStreamProperty( if (expr.questionDotToken || !isValueStream(lowerer, expr.expression)) return null; const info = lowerer.classes.get(TRANSFORM_CLASS); if (!info) return null; - return withValueStreamReceiver(lowerer, expr.expression, locOf(expr), () => + return withValueStreamReceiver(lowerer, expr.expression, locOf(expr), true, () => lowerStreamProperty(lowerer, expr, info), ); } /** Evaluates the value `receiver` names once into a hidden local and runs - * `lower` with the receiver lowering to that value's Transform. A result - * typed as the Transform (a member that answers `this`) answers the value - * itself. */ + * `lower` with the receiver lowering to that value's Transform. When + * `answersThis`, a result typed as the Transform (the member answered + * `this`) answers the value itself. */ function withValueStreamReceiver( lowerer: Lowerer, receiver: ts.Expression, loc: SrcLoc, + answersThis: boolean, lower: () => IrExpr | null, ): IrExpr | null { const value = lowerer.lowerExpr(receiver); @@ -124,7 +128,7 @@ function withValueStreamReceiver( } if (!lowered) return null; const answersStream = - lowered.type.kind === "object" && lowered.type.className === TRANSFORM_CLASS; + answersThis && lowered.type.kind === "object" && lowered.type.className === TRANSFORM_CLASS; const result: IrExpr = answersStream ? { kind: "seqExpr", diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index ab21b8a7e2..d6f89dc11e 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -2935,6 +2935,10 @@ function lowerPipelineStages( // What the next function stage receives: null before the first stage, // else the previous stream's type or the previous stage's generator. let srcT: IrType | null = null; + // The streams and stages in argument order: the head lists all streams + // before all stages, so when a stage precedes a stream the arguments + // are bound to locals first, keeping JavaScript's evaluation order. + const argOrder: { list: IrExpr[]; index: number }[] = []; for (let j = 0; j < stageArgs.length; j++) { const arg = stageArgs[j]!; const fn = pipelineStageFunction(lowerer, arg); @@ -2948,6 +2952,7 @@ function lowerPipelineStages( const source = lowerIterableSource(lowerer, arg, syncSourceFn ? fn : null, loc); if (source === null) lowerer.noLowering("pipeline over this source", arg); positions += 1; + argOrder.push({ list: stages, index: stages.length }); stages.push(source!.closure); srcT = source!.out; continue; @@ -2974,6 +2979,7 @@ function lowerPipelineStages( if (e instanceof StreamArgTypeThrow) lowerer.noLowering("pipeline over this value", arg); throw e; } + argOrder.push({ list: streams, index: streams.length }); streams.push(stream); srcT = stream.type; continue; @@ -3022,6 +3028,7 @@ function lowerPipelineStages( mask |= 1 << stages.length; } positions += 2 ** j; + argOrder.push({ list: stages, index: stages.length }); stages.push(lowered.closure); if (lowered.out.kind === "promise") { dest = 2; @@ -3048,6 +3055,20 @@ function lowerPipelineStages( } helperArgs.push(pipelineBoxHelper(lowerer, destPromise!, loc)); } + const argDecls: IrStmt[] = []; + const firstStage = argOrder.findIndex((a) => a.list === stages); + if (firstStage >= 0 && argOrder.slice(firstStage).some((a) => a.list === streams)) { + for (const a of argOrder) { + const init = a.list[a.index]!; + const local = lowerer.declareHiddenLocal("%pipelineArg", init.type); + argDecls.push({ kind: "varDecl", localId: local.id, init, loc: init.loc }); + a.list[a.index] = varRef(local.id, init.type, init.loc); + } + } + const bindArgs = (call: IrExpr): IrExpr => + 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. const flags = (dest << 1) | (mask !== 0 ? 32 : 0); const head: IrExpr[] = [ @@ -3062,7 +3083,7 @@ function lowerPipelineStages( ]; const last: IrType = dest === 2 ? { kind: "object", className: "%PassThrough" } : streams[streams.length - 1]!.type; - if (mask === 0) return { args: head, last, dest, destPromise, wrap: (call) => call }; + 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. const ac = lowerer.declareHiddenLocal("%pipelineAbort", DYN); @@ -3079,20 +3100,21 @@ function lowerPipelineStages( last, dest, destPromise, - wrap: (call) => ({ - kind: "seqExpr", - stmts: [ - { - kind: "varDecl", - localId: ac.id, - init: { kind: "libCall", fn: "fetch.abortControllerNew", args: [], type: DYN, loc }, - loc, - }, - ], - result: call, - type: call.type, - loc, - }), + wrap: (call) => + bindArgs({ + kind: "seqExpr", + stmts: [ + { + kind: "varDecl", + localId: ac.id, + init: { kind: "libCall", fn: "fetch.abortControllerNew", args: [], type: DYN, loc }, + loc, + }, + ], + result: call, + type: call.type, + loc, + }), }; } diff --git a/tests/corpus/7055-cipher-streams.ts b/tests/corpus/7055-cipher-streams.ts index 8befd23392..8f9e0d1ea7 100644 --- a/tests/corpus/7055-cipher-streams.ts +++ b/tests/corpus/7055-cipher-streams.ts @@ -8,6 +8,7 @@ import * as crypto from "node:crypto"; import { PassThrough, Readable, + Transform, Writable, addAbortSignal, compose, @@ -65,6 +66,43 @@ async function viaPipeAndForAwait(): Promise { console.log("pipe + for await:", text); } +async function viaPipeIntoTransform(): Promise { + const cipher = crypto.createCipheriv("aes-256-cbc", key, iv); + const upper = new Transform({ + transform(chunk: Buffer, _enc: string, cb: (err: Error | null, data?: Buffer) => void) { + cb(null, Buffer.from(chunk.toString("hex").toUpperCase())); + }, + }); + const out = cipher.pipe(upper); + console.log("pipe answers its destination:", out === upper); + cipher.end("into a transform"); + let hex = ""; + for await (const chunk of out) hex += chunk; + console.log("pipe into transform:", hex); +} + +async function viaPipelineArgumentOrder(): Promise { + const order: string[] = []; + const source = (): string[] => { + order.push("source"); + return ["x", "y"]; + }; + const cipherStage = (): crypto.Cipheriv => { + order.push("cipher"); + return crypto.createCipheriv("aes-256-ctr", key, iv); + }; + const hash = crypto.createHash("sha256"); + await pipelinePromise( + source(), + async function* (src: AsyncIterable) { + for await (const chunk of src) yield chunk.toUpperCase(); + }, + cipherStage(), + hash, + ); + console.log("pipeline argument order:", order.join(" "), hash.digest("hex")); +} + async function viaEvents(): Promise { const cipher = crypto.createCipheriv("aes-128-cbc", Buffer.alloc(16, 1), Buffer.alloc(16, 2)); cipher.setEncoding("hex"); @@ -176,6 +214,8 @@ async function main(): Promise { await viaCallbackPipeline(); await viaPromisePipeline(); await viaPipeAndForAwait(); + await viaPipeIntoTransform(); + await viaPipelineArgumentOrder(); await viaEvents(); await gcm(); } From 70697b45c327c515559e65cd8e4a8602bc98941b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 02:56:39 -0500 Subject: [PATCH 083/395] Give generic arrow bindings Node's behavior before their declarations run Generic arrow and function-expression bindings compile to shared instances, and their declarations emit no store. Code that can run before the declaration then diverged from Node. - Inside a function, a nested function lowered before the declaration reads the binding through a TDZ box that nothing initialized. That covers closures above the declaration and hoisted function declarations. Such a read threw a ReferenceError even after the declaration ran. The declaration now stores the instance its declared signature pins into that box. - At module scope, calls and pinned value reads went straight to the instances even when a hoisted function or an earlier closure ran them before the declaration. A binding with such a use now has a flag that its declaration sets. - Uses that can run first check the flag. - A let or const throws Node's ReferenceError before the arguments run. - A var runs the arguments and then throws "is not a function". - Uses that follow the declaration stay direct calls: later statements, closures created after it, its own initializer, and function declarations that only such code calls. A var binding inside a function that is called from a place that can run before its declaration would need a per-call flag shared with its closures. That shape now fails with SC1090 instead of calling the instance. --- .../frontend/lowering/generic-functions.ts | 330 +++++++++++++++++- .../src/frontend/lowering/lower-stmts.ts | 17 +- .../compiler/src/frontend/lowering/lowerer.ts | 3 + ...0-generic-local-binding-early-uses.ts.json | 6 + .../7181-generic-module-binding-tdz.ts.json | 6 + .../7182-generic-binding-direct-calls.ts.json | 6 + .../7180-generic-local-binding-early-uses.ts | 53 +++ .../corpus/7181-generic-module-binding-tdz.ts | 49 +++ .../7182-generic-binding-direct-calls.ts | 42 +++ tests/diagnostics/generic-value-bindings.ts | 12 + .../generic-value-bindings.ts.txt | 9 +- ...0-generic-local-binding-early-uses.ts.json | 1 + .../7181-generic-module-binding-tdz.ts.json | 1 + .../7182-generic-binding-direct-calls.ts.json | 1 + 14 files changed, 531 insertions(+), 5 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7180-generic-local-binding-early-uses.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7181-generic-module-binding-tdz.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7182-generic-binding-direct-calls.ts.json create mode 100644 tests/corpus/7180-generic-local-binding-early-uses.ts create mode 100644 tests/corpus/7181-generic-module-binding-tdz.ts create mode 100644 tests/corpus/7182-generic-binding-direct-calls.ts create mode 100644 tests/harness/corpus-costs/7180-generic-local-binding-early-uses.ts.json create mode 100644 tests/harness/corpus-costs/7181-generic-module-binding-tdz.ts.json create mode 100644 tests/harness/corpus-costs/7182-generic-binding-direct-calls.ts.json diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index abcd5a584a..d0e07582c9 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -3,15 +3,20 @@ import * as ts from "../ts7/adapter.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import type { Lowerer } from "./lowerer.js"; import { + BOOL, DYN, type IrExpr, type IrFunction, type IrParam, type IrStmt, type IrType, + type SrcLoc, + STRING, + VOID, isUnitType, typeEquals, } from "../../ir/ir.js"; +import { nodeThrowExpr } from "../../ir/build.js"; import { isJsSourceFile, locOf } from "../program.js"; import { typeKey } from "../type-mapper.js"; import { functionCanReturnUndefined } from "../function-completion.js"; @@ -22,7 +27,7 @@ import { nsPathPrefix } from "./lower-namespaces.js"; import { declSymbolOf } from "./lower-modules.js"; import { npmStaticPackageOfPath } from "../npm-static.js"; import { rejectStaticThis } from "./static-this.js"; -import { jsBindingHasOpenWrites } from "./lower-stmts.js"; +import { isVarDeclared, jsBindingHasOpenWrites } from "./lower-stmts.js"; import { type ParamShape, paramShapes, @@ -91,6 +96,15 @@ export interface GenericFnInfo { * declaration makes a new function object in JavaScript, while the * compiled instances are shared module functions, so only calls lower. */ functionScoped?: true; + /** The declaration of a generic arrow/function-expression binding. */ + bindingDecl?: ts.VariableDeclaration; + /** A module-scope binding that code can use before its declaration + * runs: the BOOL global its declaration sets, and whether it is a + * `let`/`const` (a temporal dead zone) or a `var` (undefined). */ + presence?: { flagId: string; lexical: boolean }; + /** runsAfterDeclaration's answers for the function declarations whose + * calls it placed. */ + ordering?: Map; /** IMPLICIT-ANY monomorphization (npm-static JS): parallel to * decl.parameters — the param's own symbol when the slot is a BINDABLE * implicit-any parameter (untyped, identifier-named, never written in @@ -257,7 +271,14 @@ export function lowerGenericCall( ? implicitCallInstance(lowerer, expr, info) : genericCallInstance(lowerer, expr, info); const args = lowerer.completeArgs(expr.arguments.nodes, instance.params, loc, expr); - return { kind: "call", callee: instance.name, args, type: instance.returnType, loc }; + const call: IrExpr & { kind: "call" } = { + kind: "call", + callee: instance.name, + args, + type: instance.returnType, + loc, + }; + return guardEarlyBindingCall(lowerer, info, expr, call); } /** The instance a CALL of a generic function-like names: resolved @@ -1233,6 +1254,47 @@ export function lowerGenericFnValue( } } if (!target) fenceUnpinned(); + return guardEarlyBindingValue( + lowerer, + info, + ref, + pinnedGenericClosure(lowerer, ref, info, target, fenceUnpinned), + ); +} + +/** The value a generic binding declared inside a function stores when a + * nested function read it before the declaration lowered (`const read = + * () => id; const id: (x: number) => number = (x: T): T => x`): such + * a read resolves to an ordinary TDZ local, so the declaration assigns it + * the instance its concrete declared signature pins. */ +export function genericBindingStoredValue( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + info: GenericFnInfo, +): IrExpr { + const name = decl.name as ts.Identifier; + const fenceUnpinned = (): never => + lowerer.unsupported( + "SC1090", + name, + `generic function bindings read before their declaration without a concrete declared signature (annotate '${info.baseName}' with one, or declare it before its uses)`, + ); + const sigs = lowerer.checker.getCallSignatures(lowerer.typeOf(name)); + if (sigs.length !== 1) fenceUnpinned(); + return pinnedGenericClosure(lowerer, name, info, sigs[0]!, fenceUnpinned); +} + +/** The closure of the instance of `info` that `target` pins, for a value + * read at `ref`. `fenceUnpinned` reports a signature that leaves a type + * parameter unbound. */ +function pinnedGenericClosure( + lowerer: Lowerer, + ref: ts.Expression, + info: GenericFnInfo, + target: ts.Signature, + fenceUnpinned: () => never, +): IrExpr { + const loc = locOf(ref); const bindings = new Map(); unifySignatureBindings(lowerer, info, target, bindings); const hofBinding = runtimeOptionalHofGenericBinding(lowerer, ref, info); @@ -2565,9 +2627,18 @@ export function bindingGenericFnInfoOf( typeParams, instances: new Map(), ...(functionScoped ? { functionScoped: true as const } : {}), + bindingDecl: decl, }; lowerer.bindingGenericFns.set(fnNode, info); lowerer.genericFnsBySymbol.set(sym, info); + // Module-scope uses resolve to the instances from the start, so those + // that can run first need the flag. Inside a function the declaration + // registers before any later use lowers, and earlier uses read a TDZ + // box (genericBindingStoredValue). + if (!functionScoped) { + const presence = bindingPresenceOf(lowerer, info, decl, sym); + if (presence !== undefined) info.presence = presence; + } if (ts.isFunctionExpression(fnNode) && fnNode.name !== undefined) { const inner = lowerer.checker.getSymbolAtLocation(fnNode.name); if (inner) lowerer.genericFnsBySymbol.set(inner, info); @@ -2575,6 +2646,261 @@ export function bindingGenericFnInfoOf( return info; } +/** Whether `use`, a reference to the binding `info` declares, can only + * run after the declaration has run: it follows the declaration in its + * statement list (closures count where they are created), sits inside the + * initializer itself, or is inside a function declaration whose every + * reference does. References from other modules run after this module's + * body, and export specifiers only export. A use the check cannot place + * answers false. */ +export function runsAfterDeclaration(lowerer: Lowerer, info: GenericFnInfo, use: ts.Node): boolean { + const decl = info.bindingDecl; + if (decl === undefined) return true; + info.ordering ??= new Map(); + return useRunsAfter(lowerer, decl, use, info.ordering, new Set()); +} + +function useRunsAfter( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + use: ts.Node, + memo: Map, + inProgress: Set, +): boolean { + if (ts.getSourceFileOfNode(use) !== ts.getSourceFileOfNode(decl)) return true; + const init = decl.initializer; + if (init !== undefined && use.pos >= init.pos && use.end <= init.end) return true; + const stmt = decl.parent?.parent; + const list = stmt?.parent; + if (stmt === undefined || list === undefined) return false; + for (let n: ts.Node = use; n.parent !== undefined; n = n.parent) { + if (n.parent === list) return n === stmt ? use.pos >= decl.end : n.pos >= stmt.end; + if (ts.isFunctionDeclaration(n)) return functionRunsAfter(lowerer, decl, n, memo, inProgress); + } + return false; +} + +/** Whether every call of the function declaration `fn` runs after `decl` + * has run. A function in progress counts as running after: its calls of + * itself, directly or through other functions, add no earlier caller. + * Only answers that depend on no function in progress are memoized. */ +function functionRunsAfter( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + fn: ts.FunctionDeclaration, + memo: Map, + inProgress: Set, +): boolean { + const known = memo.get(fn); + if (known !== undefined) return known; + if (inProgress.has(fn)) return true; + // `export default function () {}`: only importers call it, after this + // module's body has run. + if (fn.name === undefined) return true; + inProgress.add(fn); + let result = true; + try { + const symbol = lowerer.checker.getSymbolAtLocation(fn.name); + if (symbol === undefined) result = false; + else { + for (const ref of bindingReferences(lowerer, ts.getSourceFileOfNode(fn), symbol, fn.name)) { + if (ref.pos >= fn.pos && ref.end <= fn.end) continue; + if (!useRunsAfter(lowerer, decl, ref, memo, inProgress)) { + result = false; + break; + } + } + } + } finally { + inProgress.delete(fn); + } + if (inProgress.size === 0) memo.set(fn, result); + return result; +} + +/** The value references of `symbol` in `file` other than `name` (its + * declaration name): identifiers spelled like it that resolve to it, + * outside type positions and export specifiers. */ +function bindingReferences( + lowerer: Lowerer, + file: ts.SourceFile, + symbol: ts.Symbol, + name: ts.Identifier, +): ts.Identifier[] { + let byName = lowerer.bindingIdentifiers.get(file); + if (byName === undefined) { + byName = new Map(); + lowerer.bindingIdentifiers.set(file, byName); + } + let idents = byName.get(name.text); + if (idents === undefined) { + const found: ts.Identifier[] = []; + ts.walkPreorder(file, (node) => { + if (ts.isIdentifier(node) && node.text === name.text) found.push(node); + return undefined; + }); + lowerer.checker.prefetchSymbolNodesExact(found); + byName.set(name.text, found); + idents = found; + } + return idents.filter((ident) => { + if (ident === name) return false; + const parent = ident.parent; + if (parent === undefined || ts.isExportSpecifier(parent) || ts.isExportAssignment(parent)) + return false; + let resolved = lowerer.checker.getSymbolAtLocation(ident); + if (ts.isShorthandPropertyAssignment(parent) && parent.name === ident) + resolved = lowerer.checker.getShorthandAssignmentValueSymbol(parent) ?? resolved; + if (resolved !== symbol) return false; + for (let p: ts.Node | undefined = parent; p !== undefined; p = p.parent) { + if (ts.isTypeNode(p) && p.kind !== ts.SyntaxKind.ExpressionWithTypeArguments) return false; + if (ts.isStatement(p) || ts.isFunctionLike(p)) break; + } + return true; + }); +} + +/** The presence flag of a module-scope generic binding that code can use + * before its declaration runs (a hoisted function or an earlier closure + * called during module evaluation), or undefined when every use runs + * after it. The declaration statement sets the flag. */ +function bindingPresenceOf( + lowerer: Lowerer, + info: GenericFnInfo, + decl: ts.VariableDeclaration, + symbol: ts.Symbol, +): GenericFnInfo["presence"] { + const name = decl.name as ts.Identifier; + const early = bindingReferences(lowerer, ts.getSourceFileOfNode(decl), symbol, name).some( + (ref) => !runsAfterDeclaration(lowerer, info, ref), + ); + if (!early) return undefined; + const flagId = `%g.${info.qualifiedName}%initialized`; + lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true }); + return { flagId, lexical: !isVarDeclared(decl) }; +} + +/** The statement that records a generic binding's declaration as run, + * for a binding with a presence flag. */ +export function genericBindingPresenceStore( + info: GenericFnInfo, + decl: ts.VariableDeclaration, +): IrStmt | null { + if (info.presence === undefined) return null; + const loc = locOf(decl); + return { + kind: "assign", + localId: info.presence.flagId, + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }; +} + +/** A call of a generic binding where the binding may not be initialized + * yet. A `let`/`const` binding in its temporal dead zone throws Node's + * ReferenceError before the arguments run; an unassigned `var` is + * undefined, so the arguments run and the call throws "is not a + * function". Calls that run after the declaration stay direct. */ +function guardEarlyBindingCall( + lowerer: Lowerer, + info: GenericFnInfo, + expr: ts.CallExpression, + call: IrExpr & { kind: "call" }, +): IrExpr { + if (info.bindingDecl === undefined || runsAfterDeclaration(lowerer, info, expr)) return call; + const loc = call.loc; + const presence = requirePresence(lowerer, info, expr.expression); + const flag: IrExpr = { kind: "varRef", localId: presence.flagId, type: BOOL, loc }; + if (presence.lexical) + return { + kind: "seqExpr", + stmts: [tdzCheck(flag, info.baseName, loc)], + result: call, + type: call.type, + loc, + }; + // Arguments first, in order, then the check: Node evaluates them + // before it finds the callee is not callable. + const stmts: IrStmt[] = []; + const args = call.args.map((arg): IrExpr => { + const id = `%earlyCallArg.${lowerer.ctx.locals.length}`; + lowerer.ctx.locals.push({ id, name: "%earlyCallArg", type: arg.type, mutable: false }); + stmts.push({ kind: "varDecl", localId: id, init: arg, loc }); + return { kind: "varRef", localId: id, type: arg.type, loc }; + }); + return { + kind: "seqExpr", + stmts, + result: { + kind: "ternary", + cond: flag, + then: { ...call, args }, + else_: nodeThrowExpr(1, "", `${info.baseName} is not a function`, call.type, loc), + type: call.type, + loc, + }, + type: call.type, + loc, + }; +} + +/** A generic binding read as a value where it may not be initialized + * yet: a `let`/`const` binding checks its temporal dead zone first. */ +function guardEarlyBindingValue( + lowerer: Lowerer, + info: GenericFnInfo, + ref: ts.Expression, + value: IrExpr, +): IrExpr { + if (info.bindingDecl === undefined || runsAfterDeclaration(lowerer, info, ref)) return value; + const presence = requirePresence(lowerer, info, ref); + if (!presence.lexical) + lowerer.unsupported( + "SC1090", + ref, + `generic 'var' bindings read as values before their declaration runs (Node reads undefined there; declare '${info.baseName}' with const)`, + ); + const loc = value.loc; + const flag: IrExpr = { kind: "varRef", localId: presence.flagId, type: BOOL, loc }; + return { + kind: "seqExpr", + stmts: [tdzCheck(flag, info.baseName, loc)], + result: value, + type: value.type, + loc, + }; +} + +/** The presence flag a use that can run before the declaration checks. + * Only module-scope bindings have one: a binding inside a function would + * need a per-call flag that its closures share. */ +function requirePresence( + lowerer: Lowerer, + info: GenericFnInfo, + blame: ts.Node, +): NonNullable { + if (info.presence !== undefined) return info.presence; + return lowerer.unsupported( + "SC1090", + blame, + `generic arrow/function-expression bindings declared inside functions that code can call before their declaration runs (the binding '${info.baseName}' is a 'var' in a branch or block, or is called from a place that can run first — declare it with const before its uses)`, + ); +} + +function tdzCheck(flag: IrExpr, name: string, loc: SrcLoc): IrStmt { + return { + kind: "exprStmt", + expr: { + kind: "intrinsic", + name: "module.tdzCheck", + args: [flag, { kind: "strLit", value: name, type: STRING, loc }], + type: VOID, + loc, + }, + loc, + }; +} + /** `const h = id` — a binding ALIASING a generic function (a top-level * declaration, a registered generic binding, or another alias — resolved * left to right in declaration order). The alias registers the SAME info diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index e3480cf9c8..7d9425a28b 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -138,6 +138,8 @@ import { bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, + genericBindingPresenceStore, + genericBindingStoredValue, implicitLocalFnInfoOf, implicitLocalFnNodeOf, implicitMethodCallInfersReturn, @@ -5108,8 +5110,19 @@ export function lowerVarDecl( { const gfnNode = bindingGenericFnNodeOf(decl) ?? bindingContextualGenericFnNodeOf(lowerer, decl); if (gfnNode) { - bindingGenericFnInfoOf(lowerer, decl, gfnNode); - return null; + const info = bindingGenericFnInfoOf(lowerer, decl, gfnNode); + // A nested function that lowered before this declaration read the + // binding as an ordinary TDZ box (predeclareForwardCapture); the + // declaration initializes it, so those reads see the function once + // it runs and a ReferenceError before. + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + const pre = symbol ? lowerer.ctx.tdzPredeclared.get(symbol) : undefined; + if (pre) { + lowerer.ctx.tdzPredeclared.delete(symbol!); + const value = genericBindingStoredValue(lowerer, decl, info); + return { kind: "assign", localId: pre.id, value, initializes: true, loc: locOf(decl) }; + } + return genericBindingPresenceStore(info, decl); } } diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ee8ba7ecba..751aca523b 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1906,6 +1906,9 @@ export class Lowerer { * pinned values resolve through genericFnOf exactly like top-level * generic function declarations (bindingGenericFnInfoOf). */ readonly bindingGenericFns = new Map(); + /** Identifiers by spelling, per file, for the generic bindings' use + * ordering (runsAfterDeclaration). */ + readonly bindingIdentifiers = new Map>(); /** Per-symbol result of the never-reassigned file scan * (bindingNeverReassigned — object-literal generic-method receivers). */ readonly neverReassignedCache = new Map(); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7180-generic-local-binding-early-uses.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7180-generic-local-binding-early-uses.ts.json new file mode 100644 index 0000000000..5fb9718263 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7180-generic-local-binding-early-uses.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7180-generic-local-binding-early-uses.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7181-generic-module-binding-tdz.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7181-generic-module-binding-tdz.ts.json new file mode 100644 index 0000000000..a67424e94c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7181-generic-module-binding-tdz.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7181-generic-module-binding-tdz.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7182-generic-binding-direct-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7182-generic-binding-direct-calls.ts.json new file mode 100644 index 0000000000..b641615f88 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7182-generic-binding-direct-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7182-generic-binding-direct-calls.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7180-generic-local-binding-early-uses.ts b/tests/corpus/7180-generic-local-binding-early-uses.ts new file mode 100644 index 0000000000..85458d973e --- /dev/null +++ b/tests/corpus/7180-generic-local-binding-early-uses.ts @@ -0,0 +1,53 @@ +// Generic arrow bindings declared inside functions, used by code that runs +// before or after the declaration: closures created above the declaration +// and hoisted function declarations read the binding like any const, so a +// use before the declaration throws a ReferenceError and a later one sees +// the function. +function describe(error: unknown): string { + return error instanceof Error ? `${error.name}: ${error.message}` : String(error); +} + +function make(): (x: number) => number { + const read = (): ((x: number) => number) => id; + const id: (x: number) => number = (x: T): T => x; + return read(); +} +console.log(make()(7)); + +function outer(flag: boolean): number { + if (flag) return helper(); + const id: (x: number) => number = (x: T): T => x; + return helper(); + + function helper(): number { + return id(1); + } +} +console.log(outer(false)); +try { + console.log(outer(true)); +} catch (error) { + console.log(describe(error)); +} + +function run(): void { + const early = (): number => id(2); + try { + early(); + } catch (error) { + console.log(describe(error)); + } + const id: (x: number) => number = (x: T): T => x; + console.log(early()); +} +run(); + +function counter(): () => number { + let total = 0; + const step = (): number => (total = add(total, 1)); + const add: (a: number, b: number) => number = (a: T, b: T): number => a + b; + step(); + step(); + return step; +} +console.log(counter()()); diff --git a/tests/corpus/7181-generic-module-binding-tdz.ts b/tests/corpus/7181-generic-module-binding-tdz.ts new file mode 100644 index 0000000000..d2d2915fb6 --- /dev/null +++ b/tests/corpus/7181-generic-module-binding-tdz.ts @@ -0,0 +1,49 @@ +// A module-scope generic arrow binding is in its temporal dead zone until +// its declaration runs: a hoisted function or an earlier closure that calls +// it during module evaluation throws a ReferenceError, and calls made after +// the declaration reach the function. A `var` binding is undefined until +// then: the call's arguments run, then it throws "is not a function". +function describe(error: unknown): string { + return error instanceof Error ? `${error.name}: ${error.message}` : String(error); +} + +function early(): number { + return id(1); +} +const before = (): string => wrap("b"); + +try { + console.log(early()); +} catch (error) { + console.log(describe(error)); +} +try { + console.log(before()); +} catch (error) { + console.log(describe(error)); +} + +const id = (x: T): T => x; +let wrap = (x: T): string => `[${String(x)}]`; + +console.log(early(), before(), id("direct"), wrap(2)); + +function later(): string { + return wrap(id(3)); +} +console.log(later()); + +function callMissing(): number { + return missing(log("argument")); +} +function log(text: string): number { + console.log(text); + return 1; +} +try { + console.log(callMissing()); +} catch (error) { + console.log(describe(error)); +} +var missing = (x: T): T => x; +console.log(callMissing(), missing(5)); diff --git a/tests/corpus/7182-generic-binding-direct-calls.ts b/tests/corpus/7182-generic-binding-direct-calls.ts new file mode 100644 index 0000000000..c7b5397e12 --- /dev/null +++ b/tests/corpus/7182-generic-binding-direct-calls.ts @@ -0,0 +1,42 @@ +// Generic arrow bindings whose uses all run after the declaration: later +// statements, closures created after it, later declarators, recursion and +// function declarations that only later code calls. These calls go +// straight to the compiled instances. +const pair = (x: T): [T, T] => [x, x]; +const doubled = pair(2), labels = pair("a"); +console.log(doubled.join(","), labels.join(",")); + +function useLater(): string { + return pair(true).join(","); +} +console.log(useLater()); + +export function exported(n: number): number { + return pair(n)[0] + 1; +} +console.log(exported(4)); + +function local(): string { + var twice = (x: T): number => x * 2; + const call = (): number => twice(4); + const depth = (xs: T[], n: number): number => (n <= 0 ? xs.length : depth(xs, n - 1) + 1); + return `${twice(1)} ${call()} ${depth([1, 2], 3)} ${helper()}`; + + function helper(): string { + return pair("h").join(""); + } +} +console.log(local()); + +function loop(): string[] { + const out: string[] = []; + const later: (() => string)[] = []; + for (let i = 0; i < 3; i++) { + const tag = (x: T): string => `#${String(x)}`; + later.push(() => tag("late")); + out.push(tag(i * 10)); + } + for (const f of later) out.push(f()); + return out; +} +console.log(loop().join(" ")); diff --git a/tests/diagnostics/generic-value-bindings.ts b/tests/diagnostics/generic-value-bindings.ts index a7de7aa1b5..7c786c67ce 100644 --- a/tests/diagnostics/generic-value-bindings.ts +++ b/tests/diagnostics/generic-value-bindings.ts @@ -60,3 +60,15 @@ function throughParam(a: Amb): void { console.log(a.m(4)); } throughParam(amb); + +// A `var` binding inside a function, declared in a branch and called from +// a closure: when the branch does not run, Node finds the binding undefined. +// The compiled instances have no per-call flag for that, so the call fences. +function branchVar(flag: boolean): number { + if (flag) { + var picked = (x: T): T => x; + } + const invoke = (): number => picked(1); + return invoke(); +} +console.log(branchVar(true)); diff --git a/tests/harness/__snapshots__/generic-value-bindings.ts.txt b/tests/harness/__snapshots__/generic-value-bindings.ts.txt index 04fa9d069e..835b84fa7c 100644 --- a/tests/harness/__snapshots__/generic-value-bindings.ts.txt +++ b/tests/harness/__snapshots__/generic-value-bindings.ts.txt @@ -92,4 +92,11 @@ generic-value-bindings.ts:60:15 - error SC1090: calls of the generic method 'm' 59 | function throughParam(a: Amb): void { 60 | console.log(a.m(4)); | ^~~~~~ - 61 | } \ No newline at end of file + 61 | } + +generic-value-bindings.ts:71:32 - error SC1090: generic arrow/function-expression bindings declared inside functions that code can call before their declaration runs (the binding 'picked' is a 'var' in a branch or block, or is called from a place that can run first — declare it with const before its uses) are not supported yet + + 70 | } + 71 | const invoke = (): number => picked(1); + | ^~~~~~ + 72 | return invoke(); \ No newline at end of file diff --git a/tests/harness/corpus-costs/7180-generic-local-binding-early-uses.ts.json b/tests/harness/corpus-costs/7180-generic-local-binding-early-uses.ts.json new file mode 100644 index 0000000000..929b49d23c --- /dev/null +++ b/tests/harness/corpus-costs/7180-generic-local-binding-early-uses.ts.json @@ -0,0 +1 @@ +{ "plain": 2542, "san": 2911 } diff --git a/tests/harness/corpus-costs/7181-generic-module-binding-tdz.ts.json b/tests/harness/corpus-costs/7181-generic-module-binding-tdz.ts.json new file mode 100644 index 0000000000..321663d8b1 --- /dev/null +++ b/tests/harness/corpus-costs/7181-generic-module-binding-tdz.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 2870 } diff --git a/tests/harness/corpus-costs/7182-generic-binding-direct-calls.ts.json b/tests/harness/corpus-costs/7182-generic-binding-direct-calls.ts.json new file mode 100644 index 0000000000..e9da8d46ee --- /dev/null +++ b/tests/harness/corpus-costs/7182-generic-binding-direct-calls.ts.json @@ -0,0 +1 @@ +{ "plain": 2686, "san": 3069 } From 32da078c118daa1400cad25e5d46bbe9b07f366a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:00:21 -0500 Subject: [PATCH 084/395] Match Node's AsyncLocalStorage context in async prefixes and generator steps --- packages/compiler/src/backend/llvm/emitter.ts | 14 +-- .../compiler/test/await-call-emission.test.ts | 14 +-- .../7106-async-prefix-enter-with.ts.json | 6 ++ .../corpus/7107-for-await-context.ts.json | 6 ++ .../7108-generator-step-context.ts.json | 6 ++ packages/runtime/src/scr_async.c | 83 +++++++++++++---- packages/runtime/src/scr_async.h | 10 ++- tests/corpus/7106-async-prefix-enter-with.ts | 88 +++++++++++++++++++ tests/corpus/7107-for-await-context.ts | 37 ++++++++ tests/corpus/7108-generator-step-context.ts | 49 +++++++++++ .../7106-async-prefix-enter-with.ts.json | 1 + .../7107-for-await-context.ts.json | 1 + .../7108-generator-step-context.ts.json | 1 + 13 files changed, 281 insertions(+), 35 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7106-async-prefix-enter-with.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7107-for-await-context.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7108-generator-step-context.ts.json create mode 100644 tests/corpus/7106-async-prefix-enter-with.ts create mode 100644 tests/corpus/7107-for-await-context.ts create mode 100644 tests/corpus/7108-generator-step-context.ts create mode 100644 tests/harness/corpus-costs/7106-async-prefix-enter-with.ts.json create mode 100644 tests/harness/corpus-costs/7107-for-await-context.ts.json create mode 100644 tests/harness/corpus-costs/7108-generator-step-context.ts.json diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index c254d09077..4ab2a42fae 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1885,14 +1885,14 @@ export class LlEmitter { /** Whether a fiberless body running on its caller's stack needs the * frame scr_async_inline_enter/exit keep: it isolates the Error.stack - * frame chain, which only stack-observing programs push, and the - * AsyncLocalStorage context, which only programs that enter stores - * change (RuntimeFeatures asyncContext), as a fresh fiber would. Other - * programs run the body with a plain call. The frame also counts the - * bodies running on a fiber's stack for the runtime's trap on a fiberless - * body that suspends. */ + * frame chain, which only stack-observing programs push, as a fresh fiber + * would. Other programs run the body with a plain call. The body runs in + * its caller's AsyncLocalStorage context either way, as Node runs an async + * body up to its first await. The frame also counts the bodies running on + * a fiber's stack for the runtime's trap on a fiberless body that + * suspends. */ get fiberlessFrames(): boolean { - return this.stackTraces || this.schedulesInContext; + return this.stackTraces; } /** The spawn wrapper of a fiberless async function: runs the body on the diff --git a/packages/compiler/test/await-call-emission.test.ts b/packages/compiler/test/await-call-emission.test.ts index 96c039155c..5a967cc5f9 100644 --- a/packages/compiler/test/await-call-emission.test.ts +++ b/packages/compiler/test/await-call-emission.test.ts @@ -222,7 +222,8 @@ test("bodies that suspend only at their own awaits compile to stackless coroutin }); const framedSources = { - // Entering a store makes the frame isolate the AsyncLocalStorage context. + // Entering a store needs no frame: the body runs in its caller's context, + // as Node runs an async body's synchronous prefix. context: ` import { AsyncLocalStorage } from "node:async_hooks"; const store = new AsyncLocalStorage(); @@ -238,7 +239,7 @@ async function leaf(x: number): Promise { }`, }; -test("fiberless bodies keep their frame where it isolates a context or a stack", async () => { +test("fiberless bodies keep their frame only where it isolates a stack", async () => { for (const [name, prefix] of Object.entries(framedSources)) { const llvm = await emit(`${prefix} async function slow(x: number): Promise { @@ -262,9 +263,12 @@ async function main(): Promise { void main(); `); for (const symbol of ["sc_ai_leaf", "sc_aw_leaf", "sc_as_leaf"]) { - expect(definition(llvm, symbol), `${name} ${symbol}`).toContain( - "call void @scr_async_inline_enter(ptr %frame)", - ); + const body = definition(llvm, symbol); + if (name === "stack") + expect(body, `${name} ${symbol}`).toContain( + "call void @scr_async_inline_enter(ptr %frame)", + ); + else expect(body, `${name} ${symbol}`).not.toContain("@scr_async_inline_enter"); } } }); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7106-async-prefix-enter-with.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7106-async-prefix-enter-with.ts.json new file mode 100644 index 0000000000..2945faca5c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7106-async-prefix-enter-with.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7106-async-prefix-enter-with.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7107-for-await-context.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7107-for-await-context.ts.json new file mode 100644 index 0000000000..331f649370 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7107-for-await-context.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7107-for-await-context.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7108-generator-step-context.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7108-generator-step-context.ts.json new file mode 100644 index 0000000000..e7c5876ac3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7108-generator-step-context.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7108-generator-step-context.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index db1e54295b..9d3a254696 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -623,6 +623,10 @@ struct ScrFiber { * awaiter destroys the record when it takes the result. */ bool direct; struct ScrFiber *direct_awaiter; + /* While an async body runs its synchronous prefix (until its first await + * or its end), the AsyncLocalStorage slot of the execution that spawned + * it; NULL afterwards (scr_als_prefix_end). */ + ScrAlsCtx **prefix_slot; #ifdef SCR_ASAN_FIBERS void *fake_stack; /* Live stackless async fibers (scr_async_spawn_stackless) form a list @@ -670,6 +674,34 @@ void scr_als_ctx_release(ScrAlsCtx *c) { free(c); } +/* The end of an async body's synchronous prefix: its first await, or its + * end. Node runs that prefix inside the caller's execution, so a store it + * entered (enterWith) is still the caller's store after the call; the body + * keeps its own copy of the context for what runs after its await. */ +static void scr_als_prefix_end(ScrFiber *f) { + ScrAlsCtx **slot = f->prefix_slot; + if (slot == NULL) return; + f->prefix_slot = NULL; + if (*slot != f->als) { + scr_als_ctx_release(*slot); + *slot = scr_als_ctx_retain(f->als); + } +} + +/* A synchronous generator has no context of its own: each step runs in its + * resumer's execution, as Node runs it. A fiber-backed one switches to its + * own slot, so its step starts from the resumer's store and a store it + * enters is the resumer's after the step (scr_als_prefix_end on the way + * back); a stackless one runs in the resumer's slot (scr_gen_coro_hop). */ +static void scr_als_step_begin(ScrFiber *f) { + ScrAlsCtx **slot = SCR_ALS_SLOT(); + if (f->als != *slot) { + scr_als_ctx_release(f->als); + f->als = scr_als_ctx_retain(*slot); + } + f->prefix_slot = slot; +} + /* The lifetime region a new async fiber carries (ScrFiber.region). */ static inline void *scr_fiber_region_capture(void) { const ScrRgnFiberHooks *regions = atomic_load_explicit(&scr_region_fiber_hooks, memory_order_relaxed); @@ -1889,6 +1921,7 @@ static void scr_trampoline(void) { ScrFiber *self = scr_current; self->entry(self, self->argpack); scr_fiber_finish(self); + scr_als_prefix_end(self); /* The loop reaps the fiber that comes back to it finished, which need * not be the one it resumed: a fiber that continued in place for the * awaiters it parked (scr_await_yield) returns to the loop too. */ @@ -2010,6 +2043,7 @@ static ScrPromise *scr_async_spawn_impl(void (*entry)(ScrFiber *, void *), void /* AsyncLocalStorage: the child runs in the SPAWNER's context (Node's * init-time capture); snapshots are immutable, so a retain suffices. */ f->als = scr_als_ctx_retain(*SCR_ALS_SLOT()); + f->prefix_slot = SCR_ALS_SLOT(); f->region = scr_fiber_region_capture(); scr_fibers_live++; @@ -2023,13 +2057,15 @@ static ScrPromise *scr_async_spawn_impl(void (*entry)(ScrFiber *, void *), void #elif defined(__wasi__) (void)awaiter; ScrFiber *spawner = scr_current; + ScrAlsCtx **spawner_slot = scr_als_active; scr_current = f; scr_exc_swap_cell(&f->exc); scr_als_active = &f->als; entry(f, argpack); /* eager prefix; returns at suspend/final suspend */ + scr_als_prefix_end(f); scr_current = spawner; scr_exc_swap_cell(spawner ? &spawner->exc : NULL); - scr_als_active = spawner ? &spawner->als : &scr_als_main_slot; + scr_als_active = spawner_slot; ScrPromise *result = scr_promise_retain(f->promise); if (f->done) { scr_promise_release(f->promise); @@ -2057,6 +2093,7 @@ static ScrPromise *scr_async_spawn_impl(void (*entry)(ScrFiber *, void *), void #endif f->return_to = home; ScrFiber *spawner = scr_current; + ScrAlsCtx **spawner_slot = scr_als_active; scr_switch(home, &f->ctx, f); /* back: the fiber suspended or finished. The switch back targeted NULL * (the child doesn't know who spawned it), which pointed both the current @@ -2066,7 +2103,7 @@ static ScrPromise *scr_async_spawn_impl(void (*entry)(ScrFiber *, void *), void * `undefined` rejection while the real payload leaks). */ scr_current = spawner; scr_exc_swap_cell(spawner ? &spawner->exc : NULL); - scr_als_active = spawner ? &spawner->als : &scr_als_main_slot; + scr_als_active = spawner_slot; if (awaited != NULL) { /* Parked by the child: the loop owns (and may have reaped) it. */ if (awaiter->await_parked) return awaited; @@ -2172,6 +2209,7 @@ static ScrFiber *scr_spawn_stackless_fiber(void (*entry)(ScrFiber *, void *), vo scr_fiber_track(f); f->stackless = true; f->als = scr_als_ctx_retain(*SCR_ALS_SLOT()); + f->prefix_slot = SCR_ALS_SLOT(); /* The lifetime region the eager prefix runs in, which every later step * resumes in (scr_resume_fiber), as a fiber's body does. */ f->region = scr_fiber_region_capture(); @@ -2183,6 +2221,7 @@ static ScrFiber *scr_spawn_stackless_fiber(void (*entry)(ScrFiber *, void *), vo scr_stackless_fibers = f; #endif ScrFiber *spawner = scr_current; + ScrAlsCtx **spawner_slot = scr_als_active; #ifndef _WIN32 uintptr_t guard = scr_stack_guard; if (guard != 0 && (uintptr_t)__builtin_frame_address(0) < guard + SCR_STACKLESS_PREFIX_ROOM) { @@ -2195,9 +2234,10 @@ static ScrFiber *scr_spawn_stackless_fiber(void (*entry)(ScrFiber *, void *), vo scr_als_active = &f->als; entry(f, argpack); /* the eager prefix: returns at a suspension or the end */ } + scr_als_prefix_end(f); scr_current = spawner; scr_exc_swap_cell(spawner ? &spawner->exc : NULL); - scr_als_active = spawner ? &spawner->als : &scr_als_main_slot; + scr_als_active = spawner_slot; return f; } @@ -2303,8 +2343,10 @@ void *scr_async_spawn_await_ref(void (*entry)(ScrFiber *, void *), void *argpack * settled promise is created and settled in the same order, rejections * enter the unhandled ledger through the same settle path, and the frame * isolates what a fresh fiber would — the Error.stack frame chain - * (cell->stack) and the AsyncLocalStorage context (enterWith inside the - * body stays inside it). The caller's exception cell is clean at any call + * (cell->stack). The AsyncLocalStorage context is the caller's throughout: + * the whole body is the synchronous prefix Node runs in the caller's + * execution, so a store it enters stays entered after the call + * (scr_als_prefix_end). The caller's exception cell is clean at any call * (try/finally stashes in-flight exceptions), so a pending exception * after the body is the body's own throw. */ static void scr_inline_park_trap(void) { @@ -2321,21 +2363,12 @@ void scr_async_inline_enter(ScrAsyncInline *frame) { ScrExcCell *cell = scr_exc_current_cell(); frame->stack = cell->stack; cell->stack = NULL; - frame->als = scr_als_ctx_retain(*SCR_ALS_SLOT()); if (scr_current != NULL) scr_current->inline_depth++; } void scr_async_inline_exit(ScrAsyncInline *frame) { if (scr_current != NULL) scr_current->inline_depth--; scr_exc_current_cell()->stack = (ScrStackFrame *)frame->stack; - ScrAlsCtx *saved = (ScrAlsCtx *)frame->als; - ScrAlsCtx **slot = SCR_ALS_SLOT(); - if (*slot != saved) { - scr_als_ctx_release(*slot); - *slot = saved; /* the frame's reference moves back in */ - } else { - scr_als_ctx_release(saved); - } } ScrPromise *scr_async_inline_leave(ScrAsyncInline *frame) { @@ -2379,11 +2412,14 @@ ScrPromise *scr_async_spawn_after(ScrPromise *dependency, * switch to its own resumer: park it here, for each such awaiter up the * chain, and switch there directly. Nothing runs in between either way. */ static ScrCtx *scr_fiber_park_awaiters(ScrFiber *self) { + scr_als_prefix_end(self); ScrCtx *to = self->return_to; #ifdef SCR_SPAWN_AWAIT for (ScrFiber *cur = self; cur->awaiter != NULL;) { ScrFiber *aw = cur->awaiter; cur->awaiter = NULL; + /* The awaiter's await is its own suspension: its prefix ends here. */ + scr_als_prefix_end(aw); scr_promise_add_waiter(cur->promise, aw); aw->await_parked = true; to = aw->return_to; @@ -5675,9 +5711,13 @@ __attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bo if (retain) scr_gen_retain(g); g->state = SCR_GEN_RUNNING; f->resumer = scr_current; + ScrAlsCtx **resumer_slot = scr_als_active; scr_current = f; scr_exc_swap_cell(&f->exc); - scr_als_active = &f->als; + /* A synchronous generator has no AsyncLocalStorage context of its own: + * its step runs in the resumer's slot, as Node runs it in the execution + * that called next(). An async generator's step runs in its own. */ + if (g->is_async) scr_als_active = &f->als; if (f->coro == NULL) f->entry(f, f->argpack); else scr_coro_resume(f->coro); /* A running generator cannot be resumed again, so neither its fiber nor @@ -5686,7 +5726,7 @@ __attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bo ScrFiber *me = f->resumer; scr_current = me; scr_exc_swap_cell(me != NULL ? &me->exc : NULL); - scr_als_active = me != NULL ? &me->als : &scr_als_main_slot; + scr_als_active = resumer_slot; scr_gen_switched_back(g, f, retain, true); } @@ -5708,6 +5748,7 @@ static void scr_gen_switch_in(ScrGen *g) { #endif scr_gen_retain(g); g->state = SCR_GEN_RUNNING; + if (!g->is_async) scr_als_step_begin(f); #ifdef _WIN32 ScrCtx here = scr_win_self(); #elif defined(__wasi__) @@ -5717,6 +5758,7 @@ static void scr_gen_switch_in(ScrGen *g) { #endif f->return_to = &here; ScrFiber *me = scr_current; + ScrAlsCtx **resumer_slot = scr_als_active; #ifdef __wasi__ scr_current = f; scr_exc_swap_cell(&f->exc); @@ -5725,14 +5767,17 @@ static void scr_gen_switch_in(ScrGen *g) { else scr_wasi_coro_resume(f->coro); scr_current = me; scr_exc_swap_cell(me != NULL ? &me->exc : NULL); - scr_als_active = me != NULL ? &me->als : &scr_als_main_slot; + scr_als_active = resumer_slot; #else scr_switch(&here, &f->ctx, f); - /* Back on the consumer: restore identity + the consumer's cell (the - * yield/finish switch targeted NULL — main's cell). */ + /* Back on the consumer: restore identity, the consumer's cell and its + * AsyncLocalStorage slot (the yield/finish switch targeted NULL — main's + * cell and slot). */ scr_current = me; scr_exc_swap_cell(me != NULL ? &me->exc : NULL); + scr_als_active = resumer_slot; #endif + scr_als_prefix_end(f); scr_gen_switched_back(g, f, true, false); } diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index 827c303f97..fc01282940 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -62,12 +62,14 @@ ScrPromise *scr_async_spawn_after(ScrPromise *dependency, /* Fiberless async call (native targets): the compiler proves an async body * can never park its execution context and runs it directly on the * caller's stack between enter and leave. The frame saves what a fresh - * fiber would isolate — the Error.stack frame chain and the - * AsyncLocalStorage context — and leave restores both, returning a fresh - * pending promise (+1) the caller settles from the body's outcome. */ + * fiber would isolate, the Error.stack frame chain, and leave restores it, + * returning a fresh pending promise (+1) the caller settles from the body's + * outcome. The body runs in the caller's AsyncLocalStorage context, as + * Node runs an async body's synchronous prefix. The second word is unused; + * emitted frames keep its size. */ typedef struct ScrAsyncInline { void *stack; - void *als; + void *reserved; } ScrAsyncInline; void scr_async_inline_enter(ScrAsyncInline *frame); ScrPromise *scr_async_inline_leave(ScrAsyncInline *frame); diff --git a/tests/corpus/7106-async-prefix-enter-with.ts b/tests/corpus/7106-async-prefix-enter-with.ts new file mode 100644 index 0000000000..27dff250bd --- /dev/null +++ b/tests/corpus/7106-async-prefix-enter-with.ts @@ -0,0 +1,88 @@ +// AsyncLocalStorage.enterWith in an async callee: before the callee's first +// await it runs in the caller's execution, so the caller sees the store +// after the call (Node's semantics), and the callee keeps it after its own +// await; after the first await it stays inside the callee. Covers callees +// that never suspend, stackless and fiber-backed callees, nested callees, +// run() scopes inside the prefix, and calls without an await. +import { AsyncLocalStorage } from "node:async_hooks"; + +const store = new AsyncLocalStorage(); +const log: string[] = []; +const current = (): string => store.getStore() ?? "none"; + +async function neverSuspends(tag: string): Promise { + store.enterWith(`never-${tag}`); + return 1; +} +async function prefixThenAwait(tag: string): Promise { + store.enterWith(`prefix-${tag}`); + await null; + log.push(`callee ${tag} after its await: ${current()}`); + return 2; +} +async function afterAwait(tag: string): Promise { + await null; + store.enterWith(`after-${tag}`); + return 3; +} +async function nested(tag: string): Promise { + await neverSuspends(`inner-${tag}`); + log.push(`nested ${tag} after its callee: ${current()}`); + return 4; +} +async function runInPrefix(tag: string): Promise { + store.run(`run-${tag}`, () => store.enterWith("inside-run")); + return 5; +} +async function* ticks(): AsyncGenerator { + yield 1; +} +async function fibered(tag: string): Promise { + store.enterWith(`fibered-${tag}`); + let n = 0; + for await (const t of ticks()) n += t; + log.push(`fibered ${tag} after its loop: ${current()}`); + return n; +} + +async function main(): Promise { + await store.run("A", async () => { + await neverSuspends("1"); + log.push(`A after neverSuspends: ${current()}`); + }); + await store.run("B", async () => { + const pending = prefixThenAwait("2"); + log.push(`B after the call: ${current()}`); + store.enterWith("B-later"); + await pending; + log.push(`B after its await: ${current()}`); + }); + await store.run("C", async () => { + await afterAwait("3"); + log.push(`C after afterAwait: ${current()}`); + }); + await store.run("D", async () => { + await nested("4"); + log.push(`D after nested: ${current()}`); + }); + await store.run("E", async () => { + await runInPrefix("5"); + log.push(`E after runInPrefix: ${current()}`); + }); + await store.run("F", async () => { + void neverSuspends("6"); + log.push(`F after an unawaited call: ${current()}`); + await null; + log.push(`F later: ${current()}`); + }); + store.run("G", () => { + void neverSuspends("7"); + log.push(`G synchronous caller: ${current()}`); + }); + await store.run("H", async () => { + await fibered("8"); + log.push(`H after fibered: ${current()}`); + }); + for (const line of log) console.log(line); +} +void main(); diff --git a/tests/corpus/7107-for-await-context.ts b/tests/corpus/7107-for-await-context.ts new file mode 100644 index 0000000000..8e21933b90 --- /dev/null +++ b/tests/corpus/7107-for-await-context.ts @@ -0,0 +1,37 @@ +// An async function's AsyncLocalStorage store across `for await` over an +// async generator: the consumer keeps its store after each resumption of +// the generator, whether it came from run(), from enterWith before the +// loop, or from enterWith after an earlier await. +import { AsyncLocalStorage } from "node:async_hooks"; + +const store = new AsyncLocalStorage(); +const current = (): string => store.getStore() ?? "none"; + +async function* numbers(n: number): AsyncGenerator { + for (let i = 0; i < n; i++) { + await null; + yield i; + } +} + +async function consume(label: string): Promise { + let sum = 0; + for await (const v of numbers(3)) { + sum += v; + console.log(`${label} step ${v}: ${current()}`); + } + console.log(`${label} after the loop (${sum}): ${current()}`); +} + +async function entered(): Promise { + await null; + store.enterWith("entered"); + await consume("entered"); +} + +async function main(): Promise { + await store.run("run", () => consume("run")); + await entered(); + console.log(`main: ${current()}`); +} +void main(); diff --git a/tests/corpus/7108-generator-step-context.ts b/tests/corpus/7108-generator-step-context.ts new file mode 100644 index 0000000000..3e9a01a5ff --- /dev/null +++ b/tests/corpus/7108-generator-step-context.ts @@ -0,0 +1,49 @@ +// A synchronous generator has no AsyncLocalStorage context of its own: each +// step runs in its resumer's execution, so it sees the store current at +// that next() call, and a store it enters is the resumer's after the step. +// Covers steps inside and outside run(), for-of, spread and early return. +import { AsyncLocalStorage } from "node:async_hooks"; + +const store = new AsyncLocalStorage(); +const current = (): string => store.getStore() ?? "none"; + +function* steps(): Generator { + console.log(`start sees ${current()}`); + yield "a"; + console.log(`step 2 sees ${current()}`); + store.enterWith("entered-in-step-2"); + yield "b"; + console.log(`step 3 sees ${current()}`); +} + +function* labels(): Generator { + yield current(); + yield current(); +} + +store.run("S1", () => { + const g = steps(); + g.next(); + store.run("S2", () => g.next()); + console.log(`caller after the S2 step: ${current()}`); + store.enterWith("S3"); + g.next(); + console.log(`caller after step 3: ${current()}`); +}); + +store.run("forof", () => { + const seen: string[] = []; + for (const label of labels()) seen.push(label); + console.log(`for-of: ${seen.join(",")}`); + console.log(`spread: ${[...labels()].join(",")}`); +}); + +store.run("outer", () => { + const g = steps(); + g.next(); + g.next(); + console.log(`caller after entering step: ${current()}`); + g.return(); + console.log(`caller after return: ${current()}`); +}); +console.log(`top level: ${current()}`); diff --git a/tests/harness/corpus-costs/7106-async-prefix-enter-with.ts.json b/tests/harness/corpus-costs/7106-async-prefix-enter-with.ts.json new file mode 100644 index 0000000000..74a3101e85 --- /dev/null +++ b/tests/harness/corpus-costs/7106-async-prefix-enter-with.ts.json @@ -0,0 +1 @@ +{ "plain": 3100, "san": 4000 } diff --git a/tests/harness/corpus-costs/7107-for-await-context.ts.json b/tests/harness/corpus-costs/7107-for-await-context.ts.json new file mode 100644 index 0000000000..111fd28854 --- /dev/null +++ b/tests/harness/corpus-costs/7107-for-await-context.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 3000 } diff --git a/tests/harness/corpus-costs/7108-generator-step-context.ts.json b/tests/harness/corpus-costs/7108-generator-step-context.ts.json new file mode 100644 index 0000000000..a5e618da8e --- /dev/null +++ b/tests/harness/corpus-costs/7108-generator-step-context.ts.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 3100 } From b2b216c3df6184fd964ad3aee3ee69c07e7ecd07 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:10:12 -0500 Subject: [PATCH 085/395] Extend a generic factory's anonymous local class returned through a constructor type A generic factory declared to return a constructor type now also extends when it returns an anonymous class expression (`return class { ... } as Struct`) or a `const` bound to one (`const Hold = class { ... }; return Hold`). The result's class value is the class expression's own constructor type; before, it was read through the class's name, so an anonymous class refused with SC1090 and SC2008. --- .../frontend/lowering/generic-functions.ts | 34 ++++++++++++++----- .../corpus/6912-extend-factory-local-class.ts | 32 +++++++++++++++++ 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 0f549d889c..e843b6909f 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -496,10 +496,18 @@ function localClassResultInstance( const viaConstructorType = retTs.getSymbol()?.valueDeclaration !== resultClass; const returnType = withInstanceScope(lowerer, info, inst, () => { if (!viaConstructorType) return lowerer.mapTypeOf(retTs); - const classSymbol = resultClass.name - ? lowerer.checker.getSymbolAtLocation(resultClass.name) - : undefined; - return classSymbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(classSymbol)) : null; + // The local class's own constructor type: a class expression's type is + // its constructor type, named or not (`return class { ... } as Struct`). + const classSymbol = + !ts.isClassExpression(resultClass) && resultClass.name + ? lowerer.checker.getSymbolAtLocation(resultClass.name) + : undefined; + const classType = ts.isClassExpression(resultClass) + ? lowerer.checker.getTypeAtLocation(resultClass) + : classSymbol + ? lowerer.checker.getTypeOfSymbol(classSymbol) + : undefined; + return classType ? lowerer.mapTypeOf(classType) : null; }); if (!returnType) { fenceGenericSignatureResult(lowerer, expr, retTs); @@ -646,11 +654,19 @@ function returnedLocalClass(lowerer: Lowerer, info: GenericFnInfo): ts.ClassLike if (ts.isClassExpression(expr)) return declaredInside(expr, info.decl) ? expr : null; if (!ts.isIdentifier(expr)) return null; const declaration = lowerer.checker.getSymbolAtLocation(expr)?.valueDeclaration; - return declaration && - ts.isClassDeclaration(declaration) && - declaredInside(declaration, info.decl) - ? declaration - : null; + if (!declaration || !declaredInside(declaration, info.decl)) return null; + if (ts.isClassDeclaration(declaration)) return declaration; + // A `const` bound to a class expression (`const C = class { ... }; return C`). + if ( + ts.isVariableDeclaration(declaration) && + ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const && + declaration.initializer + ) { + let init: ts.Expression = declaration.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (ts.isClassExpression(init)) return init; + } + return null; }; if (!ts.isBlock(body)) return classOf(body as ts.Expression); let found: ts.ClassLikeDeclaration | null = null; diff --git a/tests/corpus/6912-extend-factory-local-class.ts b/tests/corpus/6912-extend-factory-local-class.ts index 8848280e27..e33a59c77e 100644 --- a/tests/corpus/6912-extend-factory-local-class.ts +++ b/tests/corpus/6912-extend-factory-local-class.ts @@ -65,3 +65,35 @@ function twice(n: number): number { return K.double() + K.quad(); } console.log(twice(3), twice(5)); + +// The factory returns an anonymous class expression, directly or through a +// `const` binding. +function anonymous(value: S): Struct { + return class { + static decode(_input: unknown): S { + return value; + } + } as unknown as Struct; +} +class Anonymous extends anonymous({ tag: "anon" }) {} +console.log(Anonymous.decode(null).tag); + +type Holder = (new (held: T) => { held: T }) & { readonly seed: () => T }; +function holder(initial: T): Holder { + const Hold = class { + held: T; + constructor(held: T) { + this.held = held; + } + static seed(): T { + return initial; + } + }; + return Hold; +} +class Boxed extends holder(7) { + twice(): number { + return this.held * 2; + } +} +console.log(new Boxed(4).twice(), Boxed.seed()); From 42dd9390c13551dd40383e3a1decbeeaa5de33af Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:10:30 -0500 Subject: [PATCH 086/395] Compute index temps of a typed-array loop counter as integers In `for (let i = 0; i < out.length; i++) { const j = i * 4; ... px[j + 1] }` the fast copy of the typed-array loop version computed `j` as a double and converted every index back. The version gives each affine index operand its range under the guard's bound on the counter, but kept the range an operand already had, and the counter of a loop to a length is only known to be below 2^53, so `i * 4` stayed a double multiply. The version now narrows the existing range to the guard's. A constant number local whose initializer has an exact integer form now keeps that integer in an i64 slot beside the double, and its reads carry both, so `px[j + 1]` indexes with integer arithmetic. Only optimized synchronous functions keep these integers, and every other declaration of the local drops its integer, since one declaration is emitted once per loop copy. --- .../backend/llvm/bytes-loop-versions.test.ts | 44 ++++++ .../src/backend/llvm/bytes-loop-versions.ts | 10 +- .../src/backend/llvm/emit-function.ts | 2 + .../src/backend/llvm/emit-statements.ts | 44 ++++++ packages/compiler/src/backend/llvm/emitter.ts | 11 ++ .../src/backend/llvm/expr-primitives.ts | 3 +- .../6996-typed-array-index-temps.ts.json | 6 + tests/corpus/6996-typed-array-index-temps.ts | 134 ++++++++++++++++++ .../6996-typed-array-index-temps.ts.json | 1 + 9 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6996-typed-array-index-temps.ts.json create mode 100644 tests/corpus/6996-typed-array-index-temps.ts create mode 100644 tests/harness/corpus-costs/6996-typed-array-index-temps.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 3fbc01cc51..28194c7b34 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -281,6 +281,50 @@ test("worker programs test each receiver's shared storage once in the guard", () expect(slow).toContain("store atomic double"); }); +test("index temps of a version's counter are integers in the fast copy", () => { + // for (let i = 0; i < out.length; i++) { const j = i * 4; out[i] = src[j] + src[j + 1] } + const length: IrExpr = { + kind: "bytesIntrinsic", + method: "length", + receiver: ref("out", f64s), + args: [], + type: F64, + loc, + }; + const loop = counted( + [ + { kind: "varDecl", localId: "at", init: bin("*", ref("i"), num(4)), loc }, + set( + ref("out", f64s), + ref("i"), + bin( + "+", + get(ref("src", f64s), ref("at")), + get(ref("src", f64s), bin("+", ref("at"), num(1))), + ), + ), + ], + length, + ); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + fn([loop], [local("at", F64)]), + ], + }; + 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(body).toContain("alloca i64 ; integer at"); + expect(fast).toMatch(/mul nsw i64 %\S+, 4/); + expect(fast).toMatch(/add nsw i64 %\S+, 1/); + expect(fast).not.toContain("fmul"); + expect(fast).not.toContain("fptosi"); + expect(fast).not.toContain("bytes.index"); +}); + 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 dcb735e619..8c7f668a00 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -427,7 +427,6 @@ export function matchBytesLoopVersion( used.add(group.check.receiver); for (const node of group.nodes) bounds.add(node); for (const operand of group.operands) { - if (facts.ranges.get(operand)) continue; const shape = shapes.get(operand)!; const a = shape.counter * counterLow, b = shape.counter * counterHigh; @@ -438,7 +437,14 @@ export function matchBytesLoopVersion( min += term.sign > 0 ? r.min : -r.max; max += term.sign > 0 ? r.max : -r.min; } - if (counterLow <= counterHigh) ranges.set(operand, { min, max }); + if (counterLow > counterHigh) continue; + // The guard's bound on the counter can narrow a range the operand + // already has (a loop to a length proves only `i < 2^53`), so that + // `i * 4` in the fast copy is exact integer arithmetic. + const known = facts.ranges.get(operand); + if (!known) ranges.set(operand, { min, max }); + else if (min > known.min || max < known.max) + ranges.set(operand, { min: Math.max(min, known.min), max: Math.min(max, known.max) }); } } // Renumber the receivers the kept checks use. diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 5053c1b540..09e6551c30 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -128,6 +128,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.integerTemps.clear(); + emitter.integerTempsEnabled = emitter.debug === null && !fn.async && !fn.generator; emitter.countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator; emitter.speculativeLoopsEnabled = emitter.countedLoopsEnabled; emitter.speculativeFunction = fn.name; diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index d2e96994bf..7f189c066c 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -50,6 +50,7 @@ import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js"; import { mangleLocal } from "../mangle.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlValue } from "./expr-context.js"; +import { widenInteger } from "./integer-values.js"; import { SCALAR_UNION_RESULT } from "./int32-abi.js"; import { emitLiteralSwitch } from "./switch-dispatch.js"; import { @@ -267,6 +268,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { else emitter.toFrame(v); B.line(`store ${emitter.llType(b.type)} ${v.name}, ptr ${b.slot}`); emitter.storeIntegerView(s.localId, v, s.init); + storeIntegerTemp(emitter, s.localId, b, v); if (isRefCounted(b.type)) { emitter.scopes[emitter.scopes.length - 1]!.push({ slot: b.slot, type: b.type }); emitter.noteLastUseMove(s, b); @@ -1544,6 +1546,48 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { * Abrupt completions inside tryBody/catchBody ride the finallyStack: * returns inline copies at the return site, while break/continue use the * shared region walk before branching to their resolved target. */ +/** A constant number local whose initializer has an exact integer form + * (such as `const j = i * 4` in a typed-array loop version) keeps that + * integer in an i64 slot beside the double, so reads such as `px[j + 1]` + * index with integer arithmetic instead of converting the double back. + * Every other declaration of the local drops the integer: the same + * declaration is emitted once per loop copy, and a copy must not read the + * integer another copy stored. */ +function storeIntegerTemp( + emitter: LlEmitter, + localId: string, + b: ReturnType, + v: LlValue, +): void { + const local = b.local; + if ( + !emitter.integerTempsEnabled || + b.kind !== "local" || + !local || + local.type.kind !== "f64" || + local.mutable || + local.tdz === true || + emitter.captureIds.has(localId) || + !v.integer + ) { + emitter.integerTemps.delete(localId); + return; + } + const B = emitter.B; + let slot = emitter.integerTemps.get(localId)?.slot; + if (slot === undefined) { + slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca i64 ; integer ${local.name}`); + } + B.line(`store i64 ${widenInteger(emitter, v.integer)}, ptr ${slot}`); + emitter.integerTemps.set(localId, { + slot, + type: "i64", + signed: true, + range: v.integer.range, + }); +} + export function emitTryCatch(emitter: LlEmitter, s: IrStmt & { kind: "tryCatch" }): void { const B = emitter.B; const hasCatch = s.catchBody !== null; diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index c254d09077..e11622b9b5 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1081,6 +1081,17 @@ export class LlEmitter { string, { slot: string; type: "i32" | "i64"; signed?: boolean; range?: { min: number; max: number } } >(); + /** Constant number locals whose latest emitted declaration stored an + * exact integer beside the double, such as `const j = i * 4` in a + * typed-array loop version: an i64 slot and the integer's range. */ + integerTemps = new Map< + string, + { slot: string; type: "i64"; signed: true; range: IntegerRange } + >(); + /** Integer temps are kept only in optimized synchronous functions: an + * async or generator body resumes with its locals in its frame, which + * an entry alloca is not part of. */ + integerTempsEnabled = false; countedLoopsEnabled = false; /** Speculative integer loops (speculative-integers.ts) run in this function. */ speculativeLoopsEnabled = false; diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 12e488b6db..354004e9ed 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -59,7 +59,8 @@ export function emitLiteralExpr( throw new InternalCompilerError(`llvm emitter bug: bare unitLit '${e.unit}'`); case "varRef": { host.materializeSplitLocal(e.localId); - const integerSlot = host.integerLoopBindings.get(e.localId); + const integerSlot = + host.integerLoopBindings.get(e.localId) ?? host.integerTemps.get(e.localId); if (integerSlot !== undefined) { const integer = B.tmp(); const number = B.tmp(); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6996-typed-array-index-temps.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6996-typed-array-index-temps.ts.json new file mode 100644 index 0000000000..b5f88af99c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6996-typed-array-index-temps.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6996-typed-array-index-temps.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6996-typed-array-index-temps.ts b/tests/corpus/6996-typed-array-index-temps.ts new file mode 100644 index 0000000000..a19561ef0a --- /dev/null +++ b/tests/corpus/6996-typed-array-index-temps.ts @@ -0,0 +1,134 @@ +// Constant index temps derived from a loop counter (`const j = i * 4`) in +// typed-array loops: as indices and as numbers, in loops whose bounds guard +// passes or fails, nested, captured by closures, read after an await or a +// yield, and in loops whose counter reaches large values. + +function gray(px: Uint8Array, out: Uint8Array): void { + for (let i = 0; i < out.length; i++) { + const j = i * 4; + out[i] = (px[j]! * 77 + px[j + 1]! * 150 + px[j + 2]! * 29) >> 8; + } +} + +function interleave(a: Float32Array, b: Float32Array, out: Float32Array): void { + for (let i = 0; i < a.length; i++) { + const at = 2 * i; + out[at] = a[i]!; + out[at + 1] = b[i]!; + } +} + +function strided(src: Int16Array, out: Int32Array): string { + const seen: number[] = []; + for (let i = 0; i < out.length; i++) { + const at = i * 3 + 2; + out[i] = src[at]! - src[at - 2]!; + if (i % 5 === 0) seen.push(at, at / 2, -at, at % 4); + } + return seen.join(","); +} + +function previous(src: Float64Array, out: Float64Array): string { + const edges: string[] = []; + for (let i = 0; i < out.length; i++) { + const before = i - 1; + out[i] = src[before]! + 1; + if (before < 1) edges.push(`${before}:${Object.is(before * 0, -0)}:${1 / (before * 0)}`); + } + return edges.join(" "); +} + +function rows(cells: Uint32Array, width: number, sums: Float64Array): void { + for (let r = 0; r < sums.length; r++) { + const base = r * width; + let total = 0; + for (let c = 0; c < width; c++) { + const at = base + c; + total += cells[at]!; + } + sums[r] = total; + } +} + +function captured(values: Uint8Array): number { + const readers: (() => number)[] = []; + for (let i = 0; i < values.length; i++) { + const at = i * 2; + readers.push(() => at + values[i]!); + } + let sum = 0; + for (const read of readers) sum += read(); + return sum; +} + +function wide(start: number, count: number): string { + const out: number[] = []; + for (let i = start; i < start + count; i++) { + const next = i + 1; + const twice = i * 2; + out.push(next, twice, next - i); + } + return out.join(","); +} + +async function afterAwait(values: Float64Array): Promise { + const out: number[] = []; + for (let i = 0; i < values.length; i++) { + const at = i * 2; + await null; + out.push(at, values[at] ?? -1); + } + return out.join(","); +} + +function* acrossYield(values: Int8Array): Generator { + for (let i = 0; i < values.length; i++) { + const at = i + 1; + yield values[i]!; + yield at * 10 + (values[at] ?? 0); + } +} + +const px = new Uint8Array(64); +for (let i = 0; i < px.length; i++) px[i] = (i * 41 + 7) & 255; +const g = new Uint8Array(16); +gray(px, g); +console.log("gray", g.join(",")); +const tall = new Uint8Array(20); +gray(px, tall); +console.log("gray past input", tall.join(",")); + +const fa = new Float32Array([1.5, -2.25, 3.125, 0.1]); +const fb = new Float32Array([10, 20, 30, 40]); +const pairs = new Float32Array(8); +interleave(fa, fb, pairs); +console.log("interleave", pairs.join(",")); +const short = new Float32Array(5); +interleave(fa, fb, short); +console.log("interleave short", short.join(",")); + +const src16 = new Int16Array(40); +for (let i = 0; i < src16.length; i++) src16[i] = (i * 9973) % 32768 - 16384; +const diff = new Int32Array(12); +console.log("strided", strided(src16, diff), diff.join(",")); +const far = new Int32Array(20); +console.log("strided past input", strided(src16, far), far.join(",")); + +const src64 = new Float64Array([0.5, 1.5, 2.5]); +const shifted = new Float64Array(4); +console.log("previous", previous(src64, shifted), shifted.join(",")); + +const cells = new Uint32Array(30); +for (let i = 0; i < cells.length; i++) cells[i] = (i * 2654435761) >>> 0; +const sums = new Float64Array(5); +rows(cells, 6, sums); +console.log("rows", sums.join(",")); +const partial = new Float64Array(6); +rows(cells, 6, partial); +console.log("rows past input", partial.join(",")); + +console.log("captured", captured(new Uint8Array([5, 6, 7]))); +console.log("wide", wide(2 ** 53 - 4, 3)); +console.log("wide negative", wide(-3, 4)); +console.log("across yield", [...acrossYield(new Int8Array([3, -4, 5]))].join(",")); +afterAwait(new Float64Array([0.25, 1.25, 2.25, 3.25, 4.25])).then((line) => console.log("after await", line)); diff --git a/tests/harness/corpus-costs/6996-typed-array-index-temps.ts.json b/tests/harness/corpus-costs/6996-typed-array-index-temps.ts.json new file mode 100644 index 0000000000..4dc6b88cbd --- /dev/null +++ b/tests/harness/corpus-costs/6996-typed-array-index-temps.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 4600 } From ec0574f44b2feb927745961916974b647121297e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:13:24 -0500 Subject: [PATCH 087/395] Read the byte offset of every untyped ArrayBuffer Buffer.from in one place All Buffer.from paths over an untyped ArrayBuffer or SharedArrayBuffer now go through one function that takes the byte offset explicitly, as Node's fromArrayBuffer does. The start-offset RangeError prints the offset as JavaScript does (-1.5, -1e+21, -Infinity) and builds its message without a fixed-size buffer, which a large negative offset overran. --- packages/runtime/src/scr_bytes_io.c | 77 ++++++++++--------- .../6868-buffer-from-untyped-array-buffer.cjs | 2 +- 2 files changed, 43 insertions(+), 36 deletions(-) diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index bd79d9089a..137208938c 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1299,20 +1299,48 @@ ScrBytes *scr_buffer_concat_len_dyn(const ScrDyn *list, double total) { return scr_buffer_concat_dyn_impl(list, total, true); } -/* Buffer.from(arrayBuffer) over a checked value: a Buffer over all of the - * ArrayBuffer or SharedArrayBuffer that shares its memory, as in Node. The - * calls that reach it have no second argument or a literal encoding name, - * which as a byte offset converts to NaN, offset 0; other second arguments - * take scr_buffer_from_dyn_offset_pooled, which applies the offset. */ -static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage) { - ScrBytes *out = scr_bytes_buffer_view(storage->v.handle.ptr, SCR_BYTES_U8, 0, false, 0); +/* Buffer.from(arrayBuffer, byteOffset) over a checked ArrayBuffer or + * SharedArrayBuffer, as Node's fromArrayBuffer: a Buffer from the offset to + * the end of the buffer that shares its memory. The offset converts with + * ToNumber, where a missing offset (NULL) or undefined and NaN are 0; an + * offset past the end throws ERR_BUFFER_OUT_OF_BOUNDS, and one at or below + * -1 V8's start-offset RangeError. NULL with the exception pending. */ +static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage, const ScrDyn *offset_value) { + double offset = 0; + if (offset_value && offset_value->kind != SCR_DYN_UNDEF) { + if (!scr_dyn_number_coerce_js(offset_value, &offset)) return NULL; + if (isnan(offset)) offset = 0; + } + ScrBytes *bytes = storage->v.handle.ptr; + ScrBytes *owner = bytes->backing ? bytes->backing : bytes; + if ((double)scr_bytes_byte_len(owner) - offset < 0) { + static const char msg[] = "\"offset\" is outside of buffer bounds"; + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS"); + return NULL; + } + double start = trunc(offset); + if (start < 0) { + /* The offset as JavaScript prints it (-1.5, -1e+21, -Infinity). */ + ScrStr *parts[3] = { + scr_str_new("Start offset ", 13), + scr_f64_to_scrstr(offset), + scr_str_new(" is outside the bounds of the buffer", 36), + }; + ScrStr *message = scr_str_concat_parts(parts, 3); + for (size_t i = 0; i < 3; i++) scr_str_release(parts[i]); + scr_throw_error(SCR_ERR_RANGE, message); + return NULL; + } + ScrBytes *out = scr_bytes_buffer_view(bytes, SCR_BYTES_U8, start, false, 0); if (out) out->is_buffer = true; return out; } ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) { if (value->kind == SCR_DYN_STR) return scr_bytes_from_str(value->v.str, encoding); - if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value); + /* The second argument here is an encoding name, which as a byte offset + * converts to NaN: offset 0. */ + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL); if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(SCR_BYTES_U8, value->v.bytes); if (value->kind == SCR_DYN_ARR) { return scr_buffer_from_array_like(value, (double)value->v.arr.len); @@ -1356,8 +1384,9 @@ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding if (value->kind == SCR_DYN_STR) return scr_buffer_from_str(value->v.str, encoding); if (value->kind == SCR_DYN_BYTES && !value->v.bytes->is_data_view) return scr_buffer_from_bytes(value->v.bytes); - /* A view of an ArrayBuffer shares its memory, so it never comes from the pool. */ - if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value); + /* A view of an ArrayBuffer shares its memory, so it never comes from the + * pool. The second argument here is an encoding name: offset 0. */ + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL); ScrBytes *out = scr_buffer_from_dyn(value, encoding); if (!out) return NULL; out->is_buffer = true; @@ -1384,9 +1413,8 @@ ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *enco /* Buffer.from(value, second) over checked values whose second argument is * not a literal encoding name: an ArrayBuffer or SharedArrayBuffer takes it - * as the byte offset, as Node's fromArrayBuffer does (undefined and NaN are - * 0, the rest of the buffer is the length, ERR_BUFFER_OUT_OF_BOUNDS past the - * end); other values convert as scr_buffer_from_dyn_ref_pooled with utf8. */ + * as the byte offset (scr_buffer_from_storage); other values convert as + * scr_buffer_from_dyn_ref_pooled with utf8. */ ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *second) { if (!scr_buffer_storage_is(value)) { ScrStr *utf8 = scr_str_new("utf8", 4); @@ -1394,28 +1422,7 @@ ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *s scr_str_release(utf8); return out; } - double offset = 0; - if (second->kind != SCR_DYN_UNDEF) { - if (!scr_dyn_number_coerce_js(second, &offset)) return NULL; - if (isnan(offset)) offset = 0; - } - ScrBytes *storage = value->v.handle.ptr; - ScrBytes *owner = storage->backing ? storage->backing : storage; - if ((double)scr_bytes_byte_len(owner) - offset < 0) { - static const char msg[] = "\"offset\" is outside of buffer bounds"; - scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS"); - return NULL; - } - double start = trunc(offset); - if (start < 0) { - char msg[96]; - int n = snprintf(msg, sizeof msg, "Start offset %.0f is outside the bounds of the buffer", start); - scr_throw_error_msg(SCR_ERR_RANGE, msg, (size_t)n); - return NULL; - } - ScrBytes *out = scr_bytes_buffer_view(storage, SCR_BYTES_U8, start, false, 0); - if (out) out->is_buffer = true; - return out; + return scr_buffer_from_storage(value, second); } /* new Buffer(number, encoding) — the deprecated ctor's string arm with a diff --git a/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs b/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs index e30b5873e0..ff2c59a46b 100644 --- a/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs +++ b/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs @@ -23,7 +23,7 @@ if (isMainThread) { const offsets = new Map(); offsets.set('storage', new ArrayBuffer(8)); const storage = offsets.get('storage'); - for (const offset of [2, 1.5, '3', NaN, null, undefined, true, 8, 9, -1, -0.5]) { + for (const offset of [2, 1.5, '3', NaN, null, undefined, true, 8, 9, -1, -0.5, -1.5, -1e21, -Number.MAX_VALUE, -Infinity]) { try { const view = Buffer.from(storage, offset); view.fill(offset === 2 ? 0x11 : 0); From c4d91bf2d9a65e65579fbc87c588beb2eadeb101 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:18:35 -0500 Subject: [PATCH 088/395] Give runtime globals definitions, export only the declared library symbols, and fence the new libFn spellings --- packages/compiler/src/frontend/lowering/surfaces.ts | 1 + packages/runtime/src/scr_json.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index adc11e35a4..64227ed084 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2263,6 +2263,7 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ "date.nativeNewFields", "date.nativeNewValue", "date.argumentsTime", + "date.classOperation", ], note: "zero arguments, one milliseconds/date-string argument, or local-time year/month fields; values are the read-only TimeClip scalar slice", }, diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 549ca91656..cab709bf65 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -37,7 +37,7 @@ static SCR_TL ScrDyn *scr_builtin_object_prototype; -const ScrDateClassHooks *scr_date_class_hooks; +const ScrDateClassHooks *scr_date_class_hooks = NULL; /* The checked-dynamic Error.prototype (scr_dyn_error_prototype) is not the * prototype of scriptc's Error instances, so a change to it would not reach From 3a879f4f05489d5915a7d49ecdf2501c7643fe6e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:24:50 -0500 Subject: [PATCH 089/395] Read error constructors as values and destructure workerData in JavaScript --- .../lowering/expressions/object-literals.ts | 25 +++++- .../lowering/expressions/property-access.ts | 20 ++++- .../src/frontend/lowering/lower-stmts.ts | 3 + .../6870-error-constructor-values.js.json | 6 ++ .../6871-worker-data-destructuring.cjs.json | 6 ++ packages/runtime/src/scr_dyn.h | 3 + packages/runtime/src/scr_dyn_invoke.c | 2 + packages/runtime/src/scr_json.c | 79 ++++++++++++++++--- tests/corpus/6870-error-constructor-values.js | 55 +++++++++++++ .../corpus/6871-worker-data-destructuring.cjs | 30 +++++++ .../6870-error-constructor-values.js.json | 1 + .../6871-worker-data-destructuring.cjs.json | 1 + 12 files changed, 216 insertions(+), 15 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json create mode 100644 tests/corpus/6870-error-constructor-values.js create mode 100644 tests/corpus/6871-worker-data-destructuring.cjs create mode 100644 tests/harness/corpus-costs/6870-error-constructor-values.js.json create mode 100644 tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 565db0cd8d..a17af9b835 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -35,6 +35,7 @@ import { tryLowerExpression } from "./try-lower-expression.js"; import { fenceSymbolFieldCopy } from "../symbol-fields.js"; import { planUnionRetag } from "../union-retag.js"; import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js"; +import { builtinGlobalValue } from "./property-access.js"; /** `{ a: 1, b: "x" }` → recordLit. The record type comes from the * contextual type when tsc has one (annotated declarations, arguments, @@ -393,9 +394,11 @@ export function lowerDynObjectLiteral( const lowerValue = (): IrExpr => ts.isMethodDeclaration(prop) ? lowerer.lowerLambda(prop, true) - : !boxValue && ts.isObjectLiteralExpression(valueExpr) - ? lowerer.lowerExprExpecting(valueExpr, DYN) - : lowerer.lowerExpr(valueExpr as ts.Expression); + : ts.isShorthandPropertyAssignment(prop) && shorthandBuiltinValue(lowerer, prop) + ? shorthandBuiltinValue(lowerer, prop)! + : !boxValue && ts.isObjectLiteralExpression(valueExpr) + ? lowerer.lowerExprExpecting(valueExpr, DYN) + : lowerer.lowerExpr(valueExpr as ts.Expression); raw = fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ?? lowerValue(); } catch (err) { // A PURE member read a JS file cannot lower (a namespace object @@ -3153,6 +3156,20 @@ function lowerDeclaredSpreadMerge( /** The value of a shorthand property (`{ x }`): the binding `x` refers to. * The property name's own symbol is the PROPERTY, so resolution goes * through getShorthandAssignmentValueSymbol. */ +/** `{ TypeError }`: a stdlib global in shorthand position reads as the value + * its name reads as elsewhere (the interned constructor or function value), + * or null when the global has no such value. The shorthand's name carries + * the property's symbol, so the identifier path cannot resolve it. */ +function shorthandBuiltinValue( + lowerer: Lowerer, + prop: ts.ShorthandPropertyAssignment, +): IrExpr | null { + const symbol = lowerer.checker.getShorthandAssignmentValueSymbol(prop); + if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; + const js = isJsSourceFile(ts.getSourceFileOfNode(prop)); + return builtinGlobalValue(ts.symbolName(symbol), locOf(prop.name), js); +} + export function lowerShorthandValue( lowerer: Lowerer, prop: ts.ShorthandPropertyAssignment, @@ -3169,6 +3186,8 @@ export function lowerShorthandValue( const callable = lowerer.lowerBuiltinCallableValue(builtin, loc); if (callable) return callable; } + const builtinValue = shorthandBuiltinValue(lowerer, prop); + if (builtinValue) return builtinValue; if (symbol) { if (lowerer.ctx.selfSymbol === symbol) { return { kind: "selfRef", type: lowerer.ctx.selfType!, loc }; diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 5cd70d2a5f..c55ec29bbd 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -4020,10 +4020,26 @@ function lowerStreamObjectProperty( return lowerStreamProperty(lowerer, expr, info); } +/** The error constructors among GLOBAL_CONSTRUCTOR_VALUES. Their statics + * (`Error.stackTraceLimit`, `Error.captureStackTrace`) keep their own + * lowerings, so fenceBuiltinConstructorStaticRead leaves them alone. */ +const ERROR_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ + "Error", + "TypeError", + "RangeError", + "SyntaxError", + "ReferenceError", + "EvalError", + "URIError", + "AggregateError", +]); + /** The builtin constructors JavaScript sources read as real function * values rather than identity tokens: the runtime interns one function per * constructor, and dynamic values answer `.constructor` with the same one, - * so `o.constructor === Object` is an exact identity test. */ + * so `o.constructor === Object` and `err.constructor === TypeError` are + * exact identity tests, and `err.constructor.name` is the constructor's + * name. */ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ "Object", "Function", @@ -4034,6 +4050,7 @@ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ "Promise", "WeakMap", "WeakSet", + ...ERROR_CONSTRUCTOR_VALUES, ]); /** The builtin global functions JavaScript sources read as interned @@ -4176,6 +4193,7 @@ function fenceBuiltinConstructorStaticRead( symbol !== undefined && ts.symbolName(symbol).endsWith("Constructor") && GLOBAL_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) && + !ERROR_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) && lowerer.isStdlibSymbol(symbol) ) lowerer.stdlibMemberFence(access); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 7f0854523c..31c0e4565f 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -3888,6 +3888,9 @@ function stdlibGlobalTokenDestructure( if (decl.initializer === undefined || !isJsSourceFile(ts.getSourceFileOfNode(decl))) return null; const globalName = stdlibGlobalNameOf(lowerer, decl.initializer); if (globalName === null) return null; + // `workerData` is a value (the data the worker was started with), not an + // API surface: its members destructure as any untyped value's do. + if (globalName === "workerData") return null; if (!ts.isObjectBindingPattern(decl.name)) { lowerer.unsupported( "SC1031", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json new file mode 100644 index 0000000000..a2f0e02ea5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6870-error-constructor-values.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json new file mode 100644 index 0000000000..b610a58788 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6871-worker-data-destructuring.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 02c4d828f2..1310d5fd88 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -725,6 +725,9 @@ ScrDyn *scr_dyn_array_constructor(void); /* The interned builtin constructor `name` (Object, Date, RegExp, Map, Set, * Promise, WeakMap, WeakSet) as a dynamic function value: +1. */ ScrDyn *scr_dyn_global_constructor(const ScrStr *name); +/* The interned function value of an error constructor ("TypeError"), or + * NULL for other names. */ +ScrDyn *scr_dyn_error_constructor(const char *name); /* A builtin global function (setTimeout, setImmediate, process.nextTick, ...) * as an interned dynamic function value: +1. */ ScrDyn *scr_dyn_builtin_function(const ScrStr *name); diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 2a010f2131..7b375018b0 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -1913,6 +1913,8 @@ static ScrDyn *dyn_function_constructor(void) { ScrDyn *scr_dyn_global_constructor(const ScrStr *name) { if (strcmp(name->data, dyn_s20) == 0) return dyn_function_constructor(); + ScrDyn *error = scr_dyn_error_constructor(name->data); + if (error) return error; ScrDyn *ctor = dyn_global_ctor(name->data); if (!ctor) scr_trap(dyn_s3); return ctor; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index b8f16630b5..e308958d71 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1799,6 +1799,55 @@ static void scr_builtin_object_prototype_cleanup(void) { scr_dyn_release(prototype); } +/* The error constructors as values (an error's `constructor`, and the + * globals in JavaScript): one interned function per constructor and thread, + * indexed like scr_error_vts, with the constructor's name and length. + * Calling one through the value is not supported yet, as for the other + * interned constructors (scr_dyn_global_constructor). */ +static const char *const scr_error_ctor_names[] = { + "Error", "TypeError", "RangeError", "SyntaxError", NULL, + "ReferenceError", "EvalError", "URIError", "AggregateError", +}; +#define SCR_ERROR_CTORS (sizeof scr_error_ctor_names / sizeof scr_error_ctor_names[0]) +static SCR_TL ScrDyn *scr_error_ctor_values[SCR_ERROR_CTORS]; +static SCR_TL bool scr_error_ctors_registered; + +static void scr_error_ctors_cleanup(void) { + for (size_t i = 0; i < SCR_ERROR_CTORS; i++) { + scr_dyn_release(scr_error_ctor_values[i]); + scr_error_ctor_values[i] = NULL; + } +} + +static ScrDyn *scr_error_ctor_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + char message[128]; + int length = snprintf(message, sizeof message, + "calling the %s constructor through a stored value is not supported yet", + (const char *)closure->fn); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, "SC2020"); + return NULL; +} + +ScrDyn *scr_dyn_error_constructor(const char *name) { + for (size_t i = 0; i < SCR_ERROR_CTORS; i++) { + if (!scr_error_ctor_names[i] || strcmp(scr_error_ctor_names[i], name) != 0) continue; + if (!scr_error_ctors_registered) { + scr_error_ctors_registered = true; + scr_atexit(scr_error_ctors_cleanup); + } + if (!scr_error_ctor_values[i]) { + uint32_t length = strcmp(name, "AggregateError") == 0 ? 2 : 1; + scr_error_ctor_values[i] = + scr_dyn_new_func(scr_closure_new((void *)scr_error_ctor_names[i], 0), scr_error_ctor_call, length, + "native:constructor", scr_error_ctor_names[i]); + } + return scr_dyn_retain(scr_error_ctor_values[i]); + } + return NULL; +} + ScrDyn *scr_dyn_object_prototype(void) { if (!scr_builtin_object_prototype) { scr_builtin_object_prototype = scr_dyn_alloc(SCR_DYN_OBJ); @@ -7704,6 +7753,17 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { return out; } case SCR_DYN_FUNC: { + /* The interned builtin constructors print with their names, as in Node + * (function Map() { [native code] }); other functions without a source + * text print the anonymous native form. */ + if (d->v.fn.sig && strcmp(d->v.fn.sig, "native:constructor") == 0 && d->v.fn.name) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "function "); + scr_jb_puts(&b, d->v.fn.name); + scr_jb_puts(&b, "() { [native code] }"); + return scr_jb_finish(&b); + } static const char f[] = "function () { [native code] }"; return scr_str_new(f, sizeof f - 1); } @@ -11657,21 +11717,18 @@ static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_ ScrDyn *own = scr_dyn_obj_get(d, key, key_len); if (own) return scr_dyn_retain(own); if (key_len == 11 && memcmp(key, "constructor", 11) == 0) { - static const char *const tokens[] = { - "[builtin Error]", "[builtin TypeError]", - "[builtin RangeError]", "[builtin SyntaxError]", - NULL, "[builtin ReferenceError]", "[builtin EvalError]", "[builtin URIError]", - "[builtin AggregateError]", + /* An error's constructor: the interned function value its global + * reads as (scr_dyn_error_constructor). */ + static const char *const names[] = { + "Error", "TypeError", "RangeError", "SyntaxError", NULL, + "ReferenceError", "EvalError", "URIError", "AggregateError", }; size_t hit = scr_errdyn_find(d, false); const ScrVt *vt = hit == SIZE_MAX ? NULL : scr_errdyn_cache[hit].err->vt; - for (size_t kind = 0; vt && kind < sizeof tokens / sizeof tokens[0]; kind++) { - if (!tokens[kind]) continue; + for (size_t kind = 0; vt && kind < sizeof names / sizeof names[0]; kind++) { + if (!names[kind]) continue; if (vt != &scr_error_vts[kind]) continue; - ScrStr *token = scr_str_new(tokens[kind], strlen(tokens[kind])); - ScrDyn *result = scr_dyn_new_str(token); - scr_str_release(token); - return result; + return scr_dyn_error_constructor(names[kind]); } } return scr_dyn_retain(scr_dyn_undefined()); diff --git a/tests/corpus/6870-error-constructor-values.js b/tests/corpus/6870-error-constructor-values.js new file mode 100644 index 0000000000..81f5086d28 --- /dev/null +++ b/tests/corpus/6870-error-constructor-values.js @@ -0,0 +1,55 @@ +// The error constructors as values in JavaScript: an error's `constructor` +// is the same function the global names, whose name, length, typeof and +// String() are Node's, for errors the program throws, errors the runtime +// throws, and errors read back from catch bindings and containers. +function describe(label, error) { + const ctor = error.constructor; + console.log(label, error.name, error.constructor.name, typeof ctor, ctor.length, String(ctor)); +} + +describe('Error', new Error('e')); +describe('TypeError', new TypeError('t')); +describe('RangeError', new RangeError('r')); +describe('SyntaxError', new SyntaxError('s')); +describe('ReferenceError', new ReferenceError('f')); +describe('EvalError', new EvalError('v')); +describe('URIError', new URIError('u')); +describe('AggregateError', new AggregateError([], 'a')); + +const thrown = [ + () => JSON.parse('{'), + () => { + const holder = JSON.parse('{"value":null}'); + return holder.value.missing; + }, + () => new Array(-1), + () => decodeURIComponent('%'), + () => { + throw new TypeError('own'); + }, +]; +const constructors = { SyntaxError, TypeError, RangeError, URIError }; +const caught = []; +for (const run of thrown) { + try { + run(); + } catch (error) { + caught.push(error); + console.log('caught', error.constructor.name, error.constructor === constructors[error.name]); + } +} + +const byName = new Map(); +for (const error of caught) byName.set(error.constructor.name, (byName.get(error.constructor.name) ?? 0) + 1); +console.log('kinds', JSON.stringify([...byName])); + +console.log('identity', new TypeError('x').constructor === TypeError, new RangeError('x').constructor === TypeError); +console.log('globals', Error.name, TypeError.length, AggregateError.length, typeof RangeError, String(SyntaxError)); + +const renamed = new TypeError('x'); +renamed.name = 'Custom'; +console.log('renamed', renamed.name, renamed.constructor.name); + +class ParseFailure extends SyntaxError {} +const failure = new ParseFailure('p'); +console.log('subclass', failure.name, failure.constructor.name, failure instanceof SyntaxError); diff --git a/tests/corpus/6871-worker-data-destructuring.cjs b/tests/corpus/6871-worker-data-destructuring.cjs new file mode 100644 index 0000000000..63ae789bdf --- /dev/null +++ b/tests/corpus/6871-worker-data-destructuring.cjs @@ -0,0 +1,30 @@ +// Destructuring the members of a worker's workerData in JavaScript: plain, +// renamed, defaulted, nested and rest bindings read the data the worker was +// started with, as reading `workerData.` does. +'use strict'; +const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads'); + +if (isMainThread) { + const sab = new SharedArrayBuffer(8); + const data = { name: 'job-7', size: 3, options: { verbose: true, tags: ['a', 'b'] }, counters: new Int32Array(sab), extra: 1, more: 'x' }; + const worker = new Worker(__filename, { workerData: data }); + worker.on('message', (lines) => { + for (const line of lines) console.log(line); + console.log('main sees', new Int32Array(sab)[0]); + }); +} else { + const lines = []; + const { name, size } = workerData; + lines.push(`plain ${name} ${size}`); + const { name: jobName, missing = 'fallback', size: count = 10 } = workerData; + lines.push(`renamed ${jobName} ${missing} ${count}`); + const { + options: { verbose, tags: [firstTag, ...otherTags] }, + } = workerData; + lines.push(`nested ${verbose} ${firstTag} ${JSON.stringify(otherTags)}`); + const { name: ignored, size: alsoIgnored, options, counters, ...rest } = workerData; + lines.push(`rest ${JSON.stringify(rest)} ${typeof options} ${ignored === name && alsoIgnored === size}`); + counters[0] = 41; + counters[0] += 1; + parentPort.postMessage(lines); +} diff --git a/tests/harness/corpus-costs/6870-error-constructor-values.js.json b/tests/harness/corpus-costs/6870-error-constructor-values.js.json new file mode 100644 index 0000000000..c608ce33a4 --- /dev/null +++ b/tests/harness/corpus-costs/6870-error-constructor-values.js.json @@ -0,0 +1 @@ +{ "plain": 1200, "san": 3400 } diff --git a/tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json b/tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json new file mode 100644 index 0000000000..d6c0cb9eea --- /dev/null +++ b/tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json @@ -0,0 +1 @@ +{ "plain": 1300, "san": 3490 } From e37b94cc94c673743d9e4eead55a6968b0a145fe Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:29:38 -0500 Subject: [PATCH 090/395] Serialize the self-hosted emitter's IR compactly in its test The pretty-printed IR of the self-hosted LLVM emitter is close to the JavaScript string length limit on CI checkouts, so any growth of the emitter fails the test with "Invalid string length". --- tests/harness/self-hosting-llvm-emitter.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/harness/self-hosting-llvm-emitter.test.ts b/tests/harness/self-hosting-llvm-emitter.test.ts index 9c24d50cb3..cb9cd69c4c 100644 --- a/tests/harness/self-hosting-llvm-emitter.test.ts +++ b/tests/harness/self-hosting-llvm-emitter.test.ts @@ -161,7 +161,7 @@ for (const backend of ["llvm"] as const) { name: string, request = llvmEmitterRequest(), ): Promise => { - writeFileSync(input, serializeModule(mod)); + writeFileSync(input, serializeModule(mod, true)); writeFileSync(config, JSON.stringify(request)); const result = await execFileAsync(stage, [input, output, config], { ...runOptions, @@ -195,7 +195,7 @@ for (const backend of ["llvm"] as const) { loc: { file: rejected.sourceFile, start: 0, end: 0 }, }, ]; - writeFileSync(input, serializeModule(rejected)); + writeFileSync(input, serializeModule(rejected, true)); writeFileSync(config, JSON.stringify(llvmEmitterRequest())); writeFileSync(output, "untouched"); const args = [input, output, config]; From 931f6d48b5952b71485b20501ead419797ffe9f3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:35:33 -0500 Subject: [PATCH 091/395] Evaluate the extends expression of classes defined inside functions A class defined inside a function whose base resolves to a known class lowered to its class value alone, so the `extends` expression never ran. Node evaluates it each time the definition runs, so its calls and other effects were missing (`class Local extends select(2) {}` never called select). The class value now follows the heritage expression's evaluation when that expression can have effects: a call, construction, assignment, update, delete, await or yield outside nested functions. Module-scope classes already evaluated it. --- .../src/frontend/lowering/lower-classes.ts | 56 ++++++++++++++++--- .../7183-local-class-heritage-effects.ts.json | 6 ++ .../7183-local-class-heritage-effects.ts | 54 ++++++++++++++++++ .../7183-local-class-heritage-effects.ts.json | 1 + 4 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7183-local-class-heritage-effects.ts.json create mode 100644 tests/corpus/7183-local-class-heritage-effects.ts create mode 100644 tests/harness/corpus-costs/7183-local-class-heritage-effects.ts.json diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 505b4d7095..7e6f80f2cb 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -6142,13 +6142,55 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara loc, }); } - return info.localClass - ? initializeRuntimeStatics( - lowerer, - info, - value.kind === "classRef" ? { ...value, captures: info.localClass.captureSources } : value, - ) - : value; + if (!info.localClass) return value; + const classValue = initializeRuntimeStatics( + lowerer, + info, + value.kind === "classRef" ? { ...value, captures: info.localClass.captureSources } : value, + ); + // The base resolves statically here, but Node still evaluates the + // heritage expression each time the class definition runs, first. + const heritage = heritageWithEffects(expr); + if (heritage === undefined) return classValue; + return { + kind: "seqExpr", + stmts: [lowerer.lowerExprStatement(heritage)], + result: classValue, + type: classValue.type, + loc: classValue.loc, + }; +} + +/** The `extends` expression of `decl` when evaluating it can have effects + * (a call, construction, assignment, update, delete, await or yield + * outside nested functions). */ +function heritageWithEffects(decl: ts.ClassLikeDeclaration): ts.Expression | undefined { + const heritage = decl.heritageClauses?.nodes.find( + (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, + )?.types?.nodes[0]?.expression; + if (heritage === undefined) return undefined; + const effects = (node: ts.Node): boolean => { + if (ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node)) + return false; + if ( + ts.isCallExpression(node) || + ts.isNewExpression(node) || + ts.isTaggedTemplateExpression(node) || + ts.isDeleteExpression(node) || + ts.isAwaitExpression(node) || + ts.isYieldExpression(node) || + (ts.isPrefixUnaryExpression(node) && + (node.operator === ts.SyntaxKind.PlusPlusToken || + node.operator === ts.SyntaxKind.MinusMinusToken)) || + ts.isPostfixUnaryExpression(node) || + (ts.isBinaryExpression(node) && + node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + node.operatorToken.kind <= ts.SyntaxKind.LastAssignment) + ) + return true; + return ts.forEachChild(node, (child) => effects(child) || undefined) ?? false; + }; + return effects(heritage) ? heritage : undefined; } /** The EXACT class a receiver expression is statically known to BE (not diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7183-local-class-heritage-effects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7183-local-class-heritage-effects.ts.json new file mode 100644 index 0000000000..8f0ebb3c23 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7183-local-class-heritage-effects.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7183-local-class-heritage-effects.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7183-local-class-heritage-effects.ts b/tests/corpus/7183-local-class-heritage-effects.ts new file mode 100644 index 0000000000..514449fdba --- /dev/null +++ b/tests/corpus/7183-local-class-heritage-effects.ts @@ -0,0 +1,54 @@ +// A class defined inside a function evaluates its `extends` expression +// every time the definition runs, even when the base it names is fixed: +// calls in the heritage run in order, and a heritage call that throws +// stops the definition. +class Base { + tag(): string { + return "base"; + } +} +const log: string[] = []; +function select(note: string): typeof Base { + log.push(`select ${note}`); + return Base; +} + +function define(round: number): string { + class Local extends select(`declaration ${round}`) {} + return new Local().tag(); +} +for (let round = 0; round < 2; round++) log.push(define(round)); + +function expressionForm(): string { + const Made = class extends select("expression") {}; + return new Made().tag(); +} +log.push(expressionForm()); + +function nested(): string { + class Outer extends select("outer") {} + class Inner extends Outer {} + return new Inner().tag(); +} +log.push(nested()); + +function refuse(): typeof Base { + throw new Error("heritage failed"); +} +function failing(): void { + try { + class Never extends refuse() {} + log.push(new Never().tag()); + } catch (error) { + log.push((error as Error).message); + } +} +failing(); + +const arrow = (n: number): string => { + class InArrow extends select(`arrow ${n}`) {} + return new InArrow().tag(); +}; +log.push(arrow(1)); + +console.log(log.join("\n")); diff --git a/tests/harness/corpus-costs/7183-local-class-heritage-effects.ts.json b/tests/harness/corpus-costs/7183-local-class-heritage-effects.ts.json new file mode 100644 index 0000000000..6bcb3bc351 --- /dev/null +++ b/tests/harness/corpus-costs/7183-local-class-heritage-effects.ts.json @@ -0,0 +1 @@ +{ "plain": 2573, "san": 3208 } From 144bfb27242bb2fc5c50f6363511f25ea895e94e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:41:43 -0500 Subject: [PATCH 092/395] Store assignment expressions into module lets declared without an initializer A module `let` declared without an initializer whose type uses its null pointer as the TDZ sentinel (a function, for example) is initialized when its declaration runs, while its slot is still empty. markUninitializedWrites marked the assignment statements that run after the declaration as initializing, but not assignments in expression position. Those ran the pointer check and threw "ReferenceError: Cannot access 'g' before initialization" where Node stores the value, so the deferred-resolver pattern `let resolve!: ...; new Promise((r) => (resolve = r))` failed, as did `(h) => (g = h)` and `const r = (g = h)`. An assignment expression to such a binding now becomes an initializing `assign` whose value a temp holds and yields. Writes that can run before the declaration keep the flag check, so they still throw Node's ReferenceError. --- .../frontend/lowering/cycle-initialization.ts | 30 +++++++-- ...ialized-let-assignment-expressions.ts.json | 6 ++ ...ninitialized-let-assignment-expressions.ts | 63 +++++++++++++++++++ ...ialized-let-assignment-expressions.ts.json | 1 + 4 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7109-uninitialized-let-assignment-expressions.ts.json create mode 100644 tests/corpus/7109-uninitialized-let-assignment-expressions.ts create mode 100644 tests/harness/corpus-costs/7109-uninitialized-let-assignment-expressions.ts.json diff --git a/packages/compiler/src/frontend/lowering/cycle-initialization.ts b/packages/compiler/src/frontend/lowering/cycle-initialization.ts index 092588a6e4..4b1e74fcf0 100644 --- a/packages/compiler/src/frontend/lowering/cycle-initialization.ts +++ b/packages/compiler/src/frontend/lowering/cycle-initialization.ts @@ -463,16 +463,38 @@ export function checkCycleBindingAccesses(lowerer: Lowerer, mod: IrModule): void /** Writes to a binding declared without an initializer that run after its * declaration store into an initialized binding whose slot may still be * empty: the pointer guard must not refuse them. Writes that can run - * earlier carry the flag check rewriteFunction inserted. */ + * earlier carry the flag check rewriteFunction inserted. An assignment in + * expression position (`(f = v)`) becomes an initializing `assign` whose + * value a temp holds and yields. */ function markUninitializedWrites(lowerer: Lowerer, mod: IrModule): void { const ids = new Set(); for (const [id, flag] of lowerer.cycleInitFlags) if (flag.uninitialized) ids.add(id); if (ids.size === 0) return; const mark = (s: IrStmt): IrStmt => s.kind === "assign" && !s.initializes && ids.has(s.localId) ? { ...s, initializes: true } : s; - const rewriteExpr = (e: IrExpr): IrExpr => mapExprChildren(e, rewriteExpr, rewriteStmt); - const rewriteStmt = (s: IrStmt): IrStmt => mark(mapStmtChildren(s, rewriteExpr, rewriteStmt)); - for (const fn of mod.functions) fn.body = fn.body.map(rewriteStmt); + for (const fn of mod.functions) { + let temps = 0; + const markExpr = (e: IrExpr): IrExpr => { + if (e.kind !== "assignExpr" || !ids.has(e.localId)) return e; + const t = `%uninitWrite.${temps++}`; + fn.locals.push({ id: t, name: "%uninitWrite", type: e.value.type, mutable: false }); + const value: IrExpr = { kind: "varRef", localId: t, type: e.value.type, loc: e.loc }; + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: t, init: e.value, loc: e.loc }, + { kind: "assign", localId: e.localId, value, initializes: true, loc: e.loc }, + ], + result: value, + type: e.type, + loc: e.loc, + }; + }; + const rewriteExpr = (e: IrExpr): IrExpr => + markExpr(mapExprChildren(e, rewriteExpr, rewriteStmt)); + const rewriteStmt = (s: IrStmt): IrStmt => mark(mapStmtChildren(s, rewriteExpr, rewriteStmt)); + fn.body = fn.body.map(rewriteStmt); + } } function rewriteFunction( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7109-uninitialized-let-assignment-expressions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7109-uninitialized-let-assignment-expressions.ts.json new file mode 100644 index 0000000000..47973fee58 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7109-uninitialized-let-assignment-expressions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7109-uninitialized-let-assignment-expressions.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7109-uninitialized-let-assignment-expressions.ts b/tests/corpus/7109-uninitialized-let-assignment-expressions.ts new file mode 100644 index 0000000000..1354ab1003 --- /dev/null +++ b/tests/corpus/7109-uninitialized-let-assignment-expressions.ts @@ -0,0 +1,63 @@ +// A module `let` declared without an initializer is initialized (to +// undefined) when its declaration runs. Assignments in expression +// position store into it like assignment statements do. + +// The deferred-resolver pattern: an expression-bodied executor. +let resolve!: (value: number) => void; +const pending = new Promise((r) => (resolve = r)); +resolve(1); +console.log("resolved", await pending); + +// An expression-bodied setter, and the assignment's value. +let handler!: (n: number) => string; +const setHandler = (h: (n: number) => string) => (handler = h); +const returned = setHandler((n) => `handled ${n}`); +console.log(returned(2), handler(3)); + +// The value held in a binding first. +let next!: () => string; +const install = (f: () => string) => { + const r = (next = f); + return r; +}; +console.log(install(() => "installed")(), next()); + +// Chained, conditional and argument positions. +let first!: (n: number) => number; +let second!: (n: number) => number; +first = second = (n) => n + 1; +console.log("chained", first(1), second(2)); +let picked!: () => string; +const choose = (flag: boolean) => (flag ? (picked = () => "yes") : (picked = () => "no")); +choose(false); +console.log("conditional", picked()); +const call = (f: () => string) => f(); +let passed!: () => string; +console.log("argument", call((passed = () => "passed")), passed()); + +// Reassignment releases the earlier function and keeps the new one. +let counter!: () => number; +const make = (start: number) => { + let n = start; + return (counter = () => ++n); +}; +make(10); +make(20); +console.log("reassigned", counter(), counter()); + +// An optional binding behaves the same way. +let maybe: ((s: string) => string) | undefined; +const setMaybe = (f: (s: string) => string) => (maybe = f); +console.log("optional", setMaybe((s) => s.toUpperCase())("a"), maybe?.("b")); + +// A write that runs before the declaration still throws. +function early(): () => number { + return (late = () => 1); +} +try { + early(); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} +let late!: () => number; +console.log("after declaration", early()(), late()); diff --git a/tests/harness/corpus-costs/7109-uninitialized-let-assignment-expressions.ts.json b/tests/harness/corpus-costs/7109-uninitialized-let-assignment-expressions.ts.json new file mode 100644 index 0000000000..46c7f78329 --- /dev/null +++ b/tests/harness/corpus-costs/7109-uninitialized-let-assignment-expressions.ts.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 4500 } From 79684d0ef491b6213c9b80bdba3a673e29650214 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:44:38 -0500 Subject: [PATCH 093/395] Name lowered helpers and closures independently of lowering order Lowering named helpers by the size of their registry at first use (%union.retag.N, %arr.map.N, ...), lifted closures and optional-chain locals by compiler-wide counters (%fnN, chain.N), generic instances by their instance count, and records and unions by creation order (rN, uN). Class expressions, mixin instances, function-local generics, non-exported namespace segments and some module globals carried the byte offset of their declaration. A name therefore depended on which unit lowered first and on the positions of unrelated code. Helpers, instances, records and unions are now named by a hash of the key they are interned by (keyedHelperName). Closures and optional-chain ids are named by their lowering unit, their structural position and a count per key within the unit. Offset-based names use the structural position: the chain of enclosing named declarations (each with its kind, name and ordinal) and the node's ordinal among same-kind nodes inside the nearest one, so an edit outside that declaration leaves the name unchanged. Interfaces and type aliases anchor the positions inside them, and default library files are named by their base name, so tsgo and tsc-ts, which install them in different directories, give the same positions. Recursive record and union placeholders are named by the printed type, the structural position of the declaration and the same for up to three levels of type arguments; a description that two types still share gets a numbered suffix for the later one. A generic class instance keyed by a literal type argument is named by the literal's text or the argument's description instead of its checker type id. Two different keys with the same name throw an InternalCompilerError. The SC6101 performance warning recognizes the new closure names (fn., with _ for a named function expression) and lists the functions that rank alike in source order. The module lists its records and unions by id. The generated program is unchanged apart from these names and that order, and test:lowering-order now fails on any name difference. --- package.json | 2 +- packages/compiler/src/contracts/check.ts | 3 +- .../lowering/array-indexed-mutation.ts | 3 +- .../src/frontend/lowering/array-values.ts | 5 +- .../frontend/lowering/builtins/arguments.ts | 3 +- .../lowering/builtins/child-process-async.ts | 3 +- .../lowering/builtins/child-process-sync.ts | 3 +- .../lowering/builtins/disposable-stack.ts | 3 +- .../frontend/lowering/builtins/filesystem.ts | 3 +- .../src/frontend/lowering/builtins/json.ts | 3 +- .../lowering/builtins/operating-system.ts | 7 +- .../frontend/lowering/builtins/performance.ts | 5 +- .../src/frontend/lowering/builtins/process.ts | 5 +- .../frontend/lowering/builtins/promises.ts | 5 +- .../lowering/builtins/promisify-callback.ts | 3 +- .../frontend/lowering/builtins/text-codecs.ts | 3 +- .../src/frontend/lowering/builtins/timers.ts | 2 +- .../src/frontend/lowering/builtins/url.ts | 7 +- .../src/frontend/lowering/builtins/v8.ts | 3 +- .../src/frontend/lowering/call-arguments.ts | 3 +- .../src/frontend/lowering/call-signatures.ts | 7 +- .../src/frontend/lowering/class-assertions.ts | 5 +- .../src/frontend/lowering/class-unions.ts | 3 +- .../lowering/coercions/function-adapters.ts | 11 +- .../frontend/lowering/coercions/structural.ts | 15 +-- .../src/frontend/lowering/coercions/unions.ts | 15 +-- .../frontend/lowering/collection-ingestion.ts | 3 +- .../lowering/containers/array-callbacks.ts | 25 ++-- .../lowering/containers/array-concat.ts | 3 +- .../lowering/containers/array-construction.ts | 7 +- .../lowering/containers/array-methods.ts | 11 +- .../lowering/containers/array-pipelines.ts | 3 +- .../src/frontend/lowering/containers/bytes.ts | 5 +- .../lowering/containers/callback-arguments.ts | 3 +- .../containers/collection-grouping.ts | 5 +- .../lowering/containers/collection-methods.ts | 15 +-- .../lowering/containers/indexed-objects.ts | 15 +-- .../lowering/containers/iterator-helpers.ts | 7 +- .../lowering/containers/string-and-regexp.ts | 11 +- .../lowering/containers/string-replacement.ts | 3 +- .../src/frontend/lowering/dynamic-tier.ts | 20 +++- .../frontend/lowering/expressions/binary.ts | 3 +- .../lowering/expressions/object-literals.ts | 7 +- .../lowering/expressions/optional-chains.ts | 10 +- .../lowering/expressions/property-access.ts | 3 +- .../lowering/expressions/relational.ts | 5 +- .../frontend/lowering/generic-functions.ts | 17 ++- .../src/frontend/lowering/intern-tables.ts | 7 +- .../frontend/lowering/iterator-adapters.ts | 17 +-- .../frontend/lowering/js-dynamic-objects.ts | 5 +- .../src/frontend/lowering/json-require.ts | 3 +- .../src/frontend/lowering/lifetime-regions.ts | 3 +- .../src/frontend/lowering/lower-assert.ts | 11 +- .../src/frontend/lowering/lower-calls.ts | 49 ++++---- .../src/frontend/lowering/lower-classes.ts | 28 ++++- .../src/frontend/lowering/lower-dgram.ts | 5 +- .../frontend/lowering/lower-event-emitter.ts | 13 +- .../src/frontend/lowering/lower-exprs.ts | 9 +- .../src/frontend/lowering/lower-functions.ts | 2 +- .../src/frontend/lowering/lower-inspect.ts | 7 +- .../src/frontend/lowering/lower-mixins.ts | 3 +- .../src/frontend/lowering/lower-mock.ts | 3 +- .../frontend/lowering/lower-modules.test.ts | 4 +- .../src/frontend/lowering/lower-modules.ts | 15 ++- .../src/frontend/lowering/lower-namespaces.ts | 11 +- .../src/frontend/lowering/lower-server.ts | 13 +- .../src/frontend/lowering/lower-stdlib.ts | 2 +- .../src/frontend/lowering/lower-stream.ts | 15 +-- .../src/frontend/lowering/lower-timers.ts | 7 +- .../src/frontend/lowering/lowerer.test.ts | 30 +++-- .../compiler/src/frontend/lowering/lowerer.ts | 107 ++++++++++++++--- .../src/frontend/lowering/native-ffi.ts | 2 +- .../lowering/object-enumeration-receiver.ts | 3 +- .../src/frontend/lowering/pairs-snapshot.ts | 3 +- .../lowering/promise-executor-adapter.ts | 3 +- .../lowering/require-initialization.ts | 3 +- .../src/frontend/lowering/stable-names.ts | 48 ++++++++ .../src/frontend/lowering/strict-equality.ts | 3 +- .../frontend/lowering/structural-positions.ts | 111 +++++++++++++++++ packages/compiler/src/frontend/ts7/ast.ts | 90 +++++++++++++- .../compiler/src/frontend/type-mapper.test.ts | 2 +- packages/compiler/src/frontend/type-mapper.ts | 93 +++++++++++++-- .../test/array-pipeline-fusion.test.ts | 2 +- .../test/format-float-arguments.test.ts | 2 +- .../test/in-runtime-key-emission.test.ts | 2 +- packages/compiler/test/stable-names.test.ts | 112 ++++++++++++++++++ .../compiler/test/type-mapper-unions.test.ts | 3 +- 87 files changed, 900 insertions(+), 262 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/stable-names.ts create mode 100644 packages/compiler/src/frontend/lowering/structural-positions.ts create mode 100644 packages/compiler/test/stable-names.test.ts diff --git a/package.json b/package.json index 5cc30d9f80..d1a8e34ae0 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test:test262": "tsx tests/test262/run.ts", "test:test262:fetch": "node tests/test262/fetch.mjs", "test:cache-identity": "node tests/harness/cache-identity.mjs", - "test:lowering-order": "node scripts/lowering-order.mjs --fail-on content", + "test:lowering-order": "node scripts/lowering-order.mjs --fail-on names", "test:sandbox": "node scripts/sandbox-test.mjs", "test:sandbox:image": "node scripts/sandbox-image.mjs", "test:watch": "vitest", diff --git a/packages/compiler/src/contracts/check.ts b/packages/compiler/src/contracts/check.ts index 5934624bf7..c804689615 100644 --- a/packages/compiler/src/contracts/check.ts +++ b/packages/compiler/src/contracts/check.ts @@ -174,7 +174,8 @@ class ContractChecker { return `${owner}.${method[2]}`; } if (fn?.sourceName !== undefined && fn.sourceName !== "") return fn.sourceName; - if (fn !== undefined && /^%fn\d+$/.test(fnName)) return `the function at ${this.where(fn.loc)}`; + if (fn !== undefined && /^%fn\.[0-9a-f]{12}$/.test(fnName)) + return `the function at ${this.where(fn.loc)}`; return fnName.replace(/^%/, ""); } diff --git a/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts b/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts index 6d539212e2..0ded460da3 100644 --- a/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts +++ b/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts @@ -9,6 +9,7 @@ import { type SrcLoc, typeEquals, } from "../../ir/ir.js"; +import { keyedHelperName } from "./stable-names.js"; import { numLit, varRef } from "../../ir/build.js"; import { typeKey } from "../type-mapper.js"; import type { Lowerer } from "./lowerer.js"; @@ -125,7 +126,7 @@ export function lowerArrayFill( writeNull; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = "%arr.fill." + lowerer.arrHofHelpers.size; + name = keyedHelperName(lowerer, "%arr.fill", key); lowerer.arrHofHelpers.set(key, name); const a = varRef("a.0", arrType, loc); const n = varRef("n.0", F64, loc); diff --git a/packages/compiler/src/frontend/lowering/array-values.ts b/packages/compiler/src/frontend/lowering/array-values.ts index 0ca8503df3..314cdfd243 100644 --- a/packages/compiler/src/frontend/lowering/array-values.ts +++ b/packages/compiler/src/frontend/lowering/array-values.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { InternalCompilerError } from "../../errors.js"; import { dynUndefinedExpr, helperLoc, varRef } from "../../ir/build.js"; import { @@ -252,7 +253,7 @@ export function lowerSafeIndexRead( let name = lowerer.arrHofHelpers.get(key); if (!name) { const at = helperLoc(loc); - name = `%arr.idxOr.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.idxOr", key); lowerer.arrHofHelpers.set(key, name); const arrT = arr.type; lowerer.liftedFns.push({ @@ -298,7 +299,7 @@ export function lowerSafeBytesRead( let name = lowerer.arrHofHelpers.get(key); if (!name) { const at = helperLoc(loc); - name = `%bytes.idxOr.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%bytes.idxOr", key); lowerer.arrHofHelpers.set(key, name); const b = varRef("b.0", bytesT, at); const i = varRef("i.0", F64, at); diff --git a/packages/compiler/src/frontend/lowering/builtins/arguments.ts b/packages/compiler/src/frontend/lowering/builtins/arguments.ts index 7f862f6031..d2fc430dd4 100644 --- a/packages/compiler/src/frontend/lowering/builtins/arguments.ts +++ b/packages/compiler/src/frontend/lowering/builtins/arguments.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { BYTES_U8, BOOL, @@ -72,7 +73,7 @@ export function lowerOptionalNumberPredicate( const key = `number.optionalPredicate:${fn}:${typeKey(widened)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%number.optionalPredicate.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%number.optionalPredicate", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", widened, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts index 5590fbe9fa..d0991dce5c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { recordToEnvPairs } from "./environment.js"; import { lowerChildArgsArg } from "./child-process.js"; import { boolLit, numLit, varRef } from "../../../ir/build.js"; @@ -218,7 +219,7 @@ function execFileAsyncHelper( const key = shell ? "execAsync" : "execFileAsync"; const existing = lowerer.valueHelpers.get(key); if (existing) return { name: existing, shapeId, envShapeId }; - const name = `%${key}.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, `%${key}`, key); lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; const envRecT: IrType = { kind: "record", shapeId: envShapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts index 24adcc1ba1..b9858e7681 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { recordToEnvPairs } from "./environment.js"; import { lowerChildArgsArg } from "./child-process.js"; import { boolLit, numLit, strLit, varRef } from "../../../ir/build.js"; @@ -541,7 +542,7 @@ function execStdioModeHelper( const key = `cp.stdiomode:${shapeId}:${fd}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%cp.stdioMode.${fd}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%cp.stdioMode.${fd}`, key); lowerer.arrHofHelpers.set(key, name); const optsT: IrType = { kind: "record", shapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts index 1b9a9c03a8..375c3f1e72 100644 --- a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts +++ b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; @@ -84,7 +85,7 @@ export function lowerDisposableStackUseCall( const key = `disposableStack.use:${className}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%disposableStack.use.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%disposableStack.use", key); lowerer.valueHelpers.set(key, helper); const context = newFnCtx(false, null, null, VOID); lowerer.fnStack.push(context); diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index 0e1c062c50..6b988c491d 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { dynUndefinedExpr, boolLit, numLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, ladderFenceExpr } from "../lowerer.js"; @@ -442,7 +443,7 @@ export function lowerStringOrBytesWrite( const key = `fs.${append ? "append" : "write"}:${dataType.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%fs.writeData.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%fs.writeData", key); lowerer.valueHelpers.set(key, helper); const body: IrStmt[] = []; for (let tag = 0; tag < arms.length; tag++) { diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index 7eb8aa9b2c..64f829865d 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { isDateJsonSource } from "./dates.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; @@ -253,7 +254,7 @@ function lowerOptionalStringifyRoot( const key = `json.optionalString:${value.type.unionId}:${JSON.stringify(indent)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%json.optionalString.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%json.optionalString", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", value.type, loc); const serialized: IrExpr = { diff --git a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts index c104650393..ee189a1f35 100644 --- a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts +++ b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { numLit, strLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; @@ -219,7 +220,7 @@ export function lowerOsCpusCall(lowerer: Lowerer, call: ts.CallExpression, loc: const key = `os.cpus:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%os.cpus.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%os.cpus", key); lowerer.valueHelpers.set(key, helper); const index = varRef("i.0", F64, loc); const field = (k: number): IrExpr => ({ @@ -346,7 +347,7 @@ export function lowerPathParseCall( const key = `${fn}:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%path.parse.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%path.parse", key); lowerer.valueHelpers.set(key, helper); const parts = varRef("parts.0", arrayOf(STRING), loc); lowerer.liftedFns.push({ @@ -448,7 +449,7 @@ export function lowerPathFormatCall( const key = `${fn}:${typeKey(recordT)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%path.format.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%path.format", key); lowerer.valueHelpers.set(key, helper); const object = varRef("object.0", recordT, loc); const read = (name: string): IrExpr => { diff --git a/packages/compiler/src/frontend/lowering/builtins/performance.ts b/packages/compiler/src/frontend/lowering/builtins/performance.ts index 72304e6a90..bae525c17c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/performance.ts +++ b/packages/compiler/src/frontend/lowering/builtins/performance.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; @@ -330,7 +331,7 @@ function lowerPerfTimelineCall( const key = `perf.${many ? "entries" : timeline.fn}:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%perf.${many ? "entries" : member}.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%perf.${many ? "entries" : member}`, key); lowerer.valueHelpers.set(key, helper); const argsRef = varRef("a.0", DYN, loc); const whichRef = varRef("w.0", F64, loc); @@ -461,7 +462,7 @@ export function perfEntryTag( const key = `perf.tag:${entryT.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%perf.tag.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%perf.tag", key); lowerer.valueHelpers.set(key, helper); const e = varRef("e.0", entryT, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 25653f5419..842f42b579 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { stdinKeypressListener } from "./readline.js"; import { envSnapshotHelper, lowerProcessLoadEnvFile } from "./environment.js"; import { dynUndefinedExpr, boolLit, numLit, strLit, varRef } from "../../../ir/build.js"; @@ -1561,7 +1562,7 @@ export function lowerProcessMethodCall( const key = `${prefix}usage.diff:${prevT.shapeId}:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%${prefix}usage.diff.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%${prefix}usage.diff`, key); lowerer.valueHelpers.set(key, helper); const pRef: IrExpr = { kind: "varRef", localId: "p.0", type: prevT, loc }; const fieldOf = (name: string): IrExpr => ({ @@ -1636,7 +1637,7 @@ export function lowerProcessMethodCall( const key = `process.memoryUsage:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%process.memoryUsage.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%process.memoryUsage", key); lowerer.valueHelpers.set(key, helper); lowerer.liftedFns.push({ name: helper, diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 668ef01f8c..75e527aa5d 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { checkedPromiseAll, thenReader } from "../checked-promise-all.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -55,7 +56,7 @@ function lowerTuplePromiseAll( const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(resultT.inner)}`; let name = lowerer.arrHofHelpers.get(`promise.allTuple:${key}`); if (name === undefined) { - name = `%promise.allTuple.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%promise.allTuple", `promise.allTuple:${key}`); lowerer.arrHofHelpers.set(`promise.allTuple:${key}`, name); const ctx = newFnCtx(false, null, null, resultT.inner); ctx.isAsync = true; @@ -557,7 +558,7 @@ export function lowerPromiseStaticCall( const key = `promise.try:${typeKey(f.type)}:${args.map((a) => typeKey(a.type)).join(",")}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%promise.try.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%promise.try", key); lowerer.arrHofHelpers.set(key, helper); const fRef: IrExpr = { kind: "varRef", localId: "f.0", type: f.type, loc }; const argLocals = args.map((a, i) => ({ id: `a${i}.0`, name: `a${i}`, type: a.type })); diff --git a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts index 822a4fecc3..b4dbbf5137 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { varRef } from "../../../ir/build.js"; @@ -170,7 +171,7 @@ function promisifyMaker(lowerer: Lowerer, shape: CallbackShape, loc: SrcLoc): st const key = `promisify:${typeKey(shape.fnT)}=>${typeKey(shape.resultT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const base = `%promisify.${lowerer.arrHofHelpers.size}`; + const base = keyedHelperName(lowerer, "%promisify", key); lowerer.arrHofHelpers.set(key, base); const { fnT, resultT, args, callbackT, callbackParamT, inner, error } = shape; const promiseT = resultT.ret; diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index 7229d5b84f..8d47c8e3af 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { isBuiltinClassInstance } from "./receiver-types.js"; import { strLit, varRef, dynUndefinedExpr } from "../../../ir/build.js"; import { staticTextDecoderEncoding } from "../text-decoder-encoding.js"; @@ -129,7 +130,7 @@ function stringDecoderHelper( const key = `strdec.${op}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%strdec.${op}.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, `%strdec.${op}`, key); lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts index 2d531663f3..d7cbd63b50 100644 --- a/packages/compiler/src/frontend/lowering/builtins/timers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts @@ -85,7 +85,7 @@ export function lowerTimersPromisesSetInterval( const genT = timersPromisesIntervalType(lowerer, expr); if (genT === null) lowerer.badType(expr, lowerer.typeOf(expr)); const yielded = lowerer.coerceInto(valueNode, value, genT.yieldT); - const fnName = `%fn${lowerer.lambdaCounter++}_tpInterval`; + const fnName = lowerer.closureName(loc, "_tpInterval"); const msParam: IrLocal = { id: "%tp.ms", name: "delay", type: F64, mutable: false }; const valueParam: IrLocal = { id: "%tp.value", name: "value", type: genT.yieldT, mutable: false }; const promiseVoid: IrType = { kind: "promise", inner: VOID }; diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts index dca8a7cc00..2a77fa85c1 100644 --- a/packages/compiler/src/frontend/lowering/builtins/url.ts +++ b/packages/compiler/src/frontend/lowering/builtins/url.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { nodeThrowExpr, countedFor, strLit, varRef, dynUndefinedExpr } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; @@ -292,7 +293,7 @@ function searchParamsFromTuples( const key = `sp.fromTuples:${row.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%sp.fromTuples.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%sp.fromTuples", key); lowerer.valueHelpers.set(key, helper); const pairsT = init.type; const sp = (): IrExpr => varRef("sp.0", SEARCH_PARAMS_T, loc); @@ -372,7 +373,7 @@ function lowerOptionalStringSearchParams( const key = `sp.optionalString:${init.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%sp.optionalString.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%sp.optionalString", key); lowerer.valueHelpers.set(key, helper); const value = varRef("init.0", init.type, loc); lowerer.liftedFns.push({ @@ -678,7 +679,7 @@ function lowerSpForEachCall( const key = `${arity}:${typeKey(fnRet)}`; let helper = lowerer.spHofHelpers.get(key); if (!helper) { - helper = `%sp.forEach.${lowerer.spHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%sp.forEach", key); lowerer.spHofHelpers.set(key, helper); lowerer.liftedFns.push(buildSpForEachFn(helper, arity, fnRet, loc)); } diff --git a/packages/compiler/src/frontend/lowering/builtins/v8.ts b/packages/compiler/src/frontend/lowering/builtins/v8.ts index 3582ce8aeb..989a61a3f0 100644 --- a/packages/compiler/src/frontend/lowering/builtins/v8.ts +++ b/packages/compiler/src/frontend/lowering/builtins/v8.ts @@ -5,6 +5,7 @@ * spirit of process.memoryUsage(): approximate values, not V8 heap * figures (a compiled binary embeds no V8 engine). Every other member * fences at its use with the by-design reason (builtinFenceHintOf). */ +import { keyedHelperName } from "../stable-names.js"; import { numLit } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; @@ -55,7 +56,7 @@ export function lowerV8HeapStatisticsCall( const key = `v8.getHeapStatistics:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%v8.getHeapStatistics.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%v8.getHeapStatistics", key); lowerer.valueHelpers.set(key, helper); const sample = (index: number): IrExpr => ({ kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index 49f7c8ec0f..060ffe9d24 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { forwardedTupleType, pureForwardedValues, stripTypeWrappers } from "./typed-arguments.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; @@ -753,7 +754,7 @@ function holeReadingLift(lowerer: Lowerer, from: IrType, to: IrType, loc: SrcLoc const key = `arr.holes:${typeKey(elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%arr.holes.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.holes", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push( buildArrayConversion( diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 34343ad270..dacb4d3246 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -849,7 +849,10 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara lowerer.genericFnsBySymbol.set(symbol, { decl, baseName: nameText, - qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText), + qualifiedName: lowerer.qualify( + ts.getSourceFileOfNode(decl), + nsPathPrefix(lowerer, decl) + nameText, + ), typeParams: [], instances: new Map(), implicitParams: implicit, @@ -897,7 +900,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara // "%default" spelling ('%' cannot appear in a user identifier). name: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(decl) + (decl.name?.text ?? "%default"), + nsPathPrefix(lowerer, decl) + (decl.name?.text ?? "%default"), ), params, returnType, diff --git a/packages/compiler/src/frontend/lowering/class-assertions.ts b/packages/compiler/src/frontend/lowering/class-assertions.ts index 900c324262..52654fab8a 100644 --- a/packages/compiler/src/frontend/lowering/class-assertions.ts +++ b/packages/compiler/src/frontend/lowering/class-assertions.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { nodeThrowExpr, varRef } from "../../ir/build.js"; import { BOOL, @@ -122,7 +123,7 @@ export function checkedClassAssertion( const key = `classassert:${typeKey(source.type)}:${typeKey(target)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%class.assert.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%class.assert", key); lowerer.coercions.retags.set(key, name); const input = varRef("value.0", source.type, loc); const body: IrStmt[] = []; @@ -228,7 +229,7 @@ export function checkedClassUnionAssertion( const key = `classunionassert:${typeKey(value.type)}:${typeKey(target)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%class.assert.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%class.assert", key); lowerer.coercions.retags.set(key, name); const input = varRef("value.0", value.type, loc); const locals = [{ id: "value.0", name: "value", type: value.type, mutable: false }]; diff --git a/packages/compiler/src/frontend/lowering/class-unions.ts b/packages/compiler/src/frontend/lowering/class-unions.ts index f52026ba42..0021b2cf89 100644 --- a/packages/compiler/src/frontend/lowering/class-unions.ts +++ b/packages/compiler/src/frontend/lowering/class-unions.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { BOOL, typeKey, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; import { nodeThrowExpr, varRef } from "../../ir/build.js"; @@ -132,7 +133,7 @@ export function narrowClassUnion( const key = `classUnion:${source.className}:${target.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%class.union.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%class.union", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", source, loc); // Rank by ancestry before sorting: returning zero for unrelated pairs diff --git a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts index ffdac19f0d..fe3c14605c 100644 --- a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts +++ b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; import { buildFunctionAdapter } from "./builders.js"; import { InternalCompilerError } from "../../../errors.js"; @@ -33,7 +34,7 @@ export function funcReturnWidthAdapter( const key = `fn:${typeKey(fromT.ret.elem)}:${typeKey(toT.ret.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%fn.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%fn.width", key); lowerer.valueHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); // The returned closure's body: call the captured original, width-map. @@ -163,7 +164,7 @@ function restPackingAdapter( const key = `fnrestadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%fn.adapt.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%fn.adapt", key); lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const fixed = fromT.params.length - 1; @@ -220,7 +221,7 @@ function funcIntoDynRestAdapter( const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%fn.adapt.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%fn.adapt", key); lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = [{ localId: "rest.0", name: "args", type: DYN }]; @@ -376,7 +377,7 @@ export function funcCoerceAdapter( const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%fn.adapt.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%fn.adapt", key); lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = toT.params.map((t, i) => ({ @@ -604,7 +605,7 @@ export function spawnResFnAdapter( const key = `fnspawn:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%fnval.spawnres.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%fnval.spawnres", key); lowerer.valueHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = toT.params.map((p, i) => ({ diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 496a296074..5b5e283b32 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { constructedValue } from "../constructor-returns.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import type { FieldLift } from "./structural-plans.js"; @@ -253,7 +254,7 @@ export function unionRecordWidthHelper( const key = `unionWidth:${fromId}:${target.shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%union.record.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%union.record", key); lowerer.valueHelpers.set(key, name); const type: IrType = { kind: "union", unionId: fromId }; const value = varRef("value.0", type, loc); @@ -312,7 +313,7 @@ export function recordWidthHelper( const key = `rec:${fromId}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%rec.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%rec.width", key); // Interned BEFORE the body builds: a recursive nested-width field // (self-referential shapes) resolves to this helper itself. lowerer.valueHelpers.set(key, name); @@ -341,7 +342,7 @@ export function tupleArrayWidthHelper( const key = `tuparr:${fromId}:${typeKey(toT.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%tup.arr.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%tup.arr", key); lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "record", shapeId: fromId }; const t: IrExpr = { kind: "varRef", localId: "t.0", type: fromT, loc }; @@ -388,7 +389,7 @@ export function emptyArrayLiftHelper( const key = `emptyarr:${typeKey(fromT.elem)}:${typeKey(toT.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%arr.empty.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.empty", key); lowerer.valueHelpers.set(key, name); const a: IrExpr = { kind: "varRef", localId: "a.0", type: fromT, loc }; lowerer.liftedFns.push({ @@ -452,7 +453,7 @@ export function arrayWidthHelper( const key = `arr:${typeKey(fromElem)}:${typeKey(toElem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%arr.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.width", key); lowerer.valueHelpers.set(key, name); const arrT: IrType = { kind: "array", elem: fromElem }; const outT: IrType = { kind: "array", elem: toElem }; @@ -492,7 +493,7 @@ export function objRecordWidthHelper( const key = `obj:${className}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%obj.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%obj.width", key); lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "object", className }; lowerer.liftedFns.push(buildRecordProjection(lowerer, name, fromT, to, plan, loc)); @@ -517,7 +518,7 @@ export function recordClassWidthHelper( const key = `cls:${fromId}:${className}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%cls.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%cls.width", key); lowerer.valueHelpers.set(key, name); lowerer.noteEdge(`%${className}.constructor`); const fromT: IrType = { kind: "record", shapeId: fromId }; diff --git a/packages/compiler/src/frontend/lowering/coercions/unions.ts b/packages/compiler/src/frontend/lowering/coercions/unions.ts index 06c0af9c6b..434e687a50 100644 --- a/packages/compiler/src/frontend/lowering/coercions/unions.ts +++ b/packages/compiler/src/frontend/lowering/coercions/unions.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { buildUnionNarrow, unionNarrowFailureMessage } from "../union-narrow.js"; import { isSafeToRepeat } from "../expressions/evaluation-safety.js"; import { nodeThrowExpr } from "../../../ir/build.js"; @@ -147,7 +148,7 @@ export function strandedUnitTrap( const key = `strandunit:${typeKey(expected)}:${expr.type.kind}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%unit.strand.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%unit.strand", key); lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push({ name, @@ -227,7 +228,7 @@ export function strandedCoercionTrap( const key = `strand:${expected.unionId}:${typeKey(src)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%union.strand.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%union.strand", key); lowerer.coercions.retags.set(key, name); const toT: IrType = { kind: "union", unionId: expected.unionId }; lowerer.liftedFns.push({ @@ -275,7 +276,7 @@ export function recordUnionWrapHelper( const key = `recordWrap:${source.shapeId}:${toId}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%record.wrap.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%record.wrap", key); lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) => @@ -307,7 +308,7 @@ export function recordUnionRouteHelper( (from, dst) => lowerer.widthLiftPlan(from, dst), ); if (!plan) return null; - const name = `%record.route.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%record.route", key); lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) => @@ -352,7 +353,7 @@ export function unionRetagHelper( }); const key = `${fromId}:${toId}:${stranded.join(".")}`; const existing = lowerer.coercions.retags.get(key); - const name = existing ?? `%union.retag.${lowerer.coercions.retags.size}`; + const name = existing ?? keyedHelperName(lowerer, "%union.retag", key); if (!existing) { lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( @@ -410,7 +411,7 @@ export function narrowedArmHelper( lowerer.coercions.checkedNarrows.add(existing); return existing; } - const name = `%union.narrow.${lowerer.coercions.narrows.size}`; + const name = keyedHelperName(lowerer, "%union.narrow", key); lowerer.coercions.narrows.set(key, name); lowerer.coercions.checkedNarrows.add(name); const fn = buildUnionNarrow(name, from, target, loc, (type) => lowerer.fmt(type)); @@ -587,7 +588,7 @@ export function deferredReadHelper( const key = `deferred:${fromId}:${tag}`; const existing = lowerer.coercions.narrows.get(key); if (existing) return existing; - const name = `%deferred.read.${lowerer.coercions.narrows.size}`; + const name = keyedHelperName(lowerer, "%deferred.read", key); lowerer.coercions.narrows.set(key, name); const dflt: IrExpr = target.kind === "bool" diff --git a/packages/compiler/src/frontend/lowering/collection-ingestion.ts b/packages/compiler/src/frontend/lowering/collection-ingestion.ts index 5d14d54925..8fe2a6bbbf 100644 --- a/packages/compiler/src/frontend/lowering/collection-ingestion.ts +++ b/packages/compiler/src/frontend/lowering/collection-ingestion.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { InternalCompilerError } from "../../errors.js"; import { typeKey } from "../type-mapper.js"; @@ -197,7 +198,7 @@ export function ingestCollection( const key = `ingest:${input.collection ? typeKey(input.collection) + ":" + input.selection : "cursor:" + typeKey(input.element)}:${typeKey(destination)}:${mapper ? typeKey(mapper.type) : "copy"}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%collection.ingest.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%collection.ingest", key); lowerer.arrHofHelpers.set(key, name); const context = newFnCtx(false, null, null, destination); lowerer.fnStack.push(context); diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index eb99592bb9..7ca69d9ba3 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js"; import { transformStmtList } from "../../../ir/traverse.js"; import type { ScrDiagnostic } from "../../../diagnostics/diagnostic.js"; @@ -506,7 +507,7 @@ function arrayHofHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}:${typeKey(outElem)}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method, elem, fnRet, arity, outElem, present, split: !!fastT }); const fn = buildArrayHofFn( @@ -619,7 +620,7 @@ function arrayUnionHofHelper( const key = `union:${method}:${receiverT.unionId}:${typeKey(callbackT)}:${typeKey(resultT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.union.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.union", key); lowerer.arrHofHelpers.set(key, name); const receiver = varRef("a.0", receiverT, loc); const callback = varRef("f.0", callbackT, loc); @@ -1204,7 +1205,7 @@ function findHelper( const key = `${method}:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}${site}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); const valueT = callbackValueType(lowerer, elem, present); @@ -1304,7 +1305,7 @@ function findIndexHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); if (!last) lowerer.arrHofInfo.set(name, { @@ -1388,7 +1389,7 @@ function someEveryHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method, @@ -1587,7 +1588,7 @@ function flatMapHelper( const key = `flatMap:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.flatMap.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.flatMap", key); lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method: "flatMapArray", @@ -1829,7 +1830,7 @@ function reduceHelper( const key = `${method}:${typeKey(elem)}:${typeKey(accT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : "seed"}${site}${fastT ? ":split" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); if (method === "reduce") lowerer.arrHofInfo.set(name, { @@ -2121,7 +2122,7 @@ export function lowerArraySortCall( const key = `${method}:${typeKey(elem)}:${arity}${native ? ":native" : ""}${keyed ? `:keyed${order}${reads ? `:field${reads.length}` : ""}` : ""}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const undefinedTag = elem.kind === "union" ? lowerer.armTag(elem.unionId, UNDEFINED_T) : -1; lowerer.liftedFns.push( @@ -2167,7 +2168,7 @@ function sortKeyReader( const cacheKey = `sortKey:${typeKey(elem)}:${sortKeysId(reads.map((read) => ({ order: 1, read })))}`; let reader = lowerer.arrHofHelpers.get(cacheKey); if (!reader) { - reader = `%arr.sortKey.${lowerer.arrHofHelpers.size}`; + reader = keyedHelperName(lowerer, "%arr.sortKey", cacheKey); lowerer.arrHofHelpers.set(cacheKey, reader); lowerer.liftedFns.push(buildSortKeyFn(reader, elem, reads, loc)); } @@ -2308,7 +2309,7 @@ function dynFlatMapHelper(lowerer: Lowerer, elemT: IrType, arity: number, loc: S const key = `dynflatmap:${typeKey(elemT)}:${arity}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%dyn.flatmap.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%dyn.flatmap", key); lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push(buildDynFlatMapFn(name, elemT, arity, loc)); return name; @@ -2481,7 +2482,7 @@ function dynFilterHelper(lowerer: Lowerer, elemT: IrType, arity: number, loc: Sr const key = `dynfilter:${typeKey(elemT)}:${arity}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%dyn.filter.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%dyn.filter", key); lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push(buildDynFilterFn(name, elemT, arity, loc)); return name; @@ -2676,7 +2677,7 @@ export function lowerTuplePredicateCall( const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(fnArg.type)}:${typeKey(resultT)}:${arity}${callbackSite(fnArg)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%tuple.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const v = varRef("v.0", elem, loc); const out = varRef("out.0", resultT, loc); diff --git a/packages/compiler/src/frontend/lowering/containers/array-concat.ts b/packages/compiler/src/frontend/lowering/containers/array-concat.ts index 4f6e4ca437..b4e285ee98 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-concat.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-concat.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { identityPreservingWidening } from "../coercions/identity.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import { @@ -28,7 +29,7 @@ export function arrayConcatHelper( const key = `concat:${typeKey(elem)}:${shape.map((type) => (type ? `a:${typeKey(type)}` : "e")).join(";")}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.concat.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.concat", key); const arrT = arrayOf(elem); const params = [ { localId: "a.0", name: "a", type: arrT }, diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 40e4d64367..1f10640c03 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { generatorDrain } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; import { liftedFreshSpread } from "../coercions/fresh-spread.js"; @@ -256,7 +257,7 @@ function appendWidenedArrayValues( const key = `insertValues:${typeKey(sourceType.elem)}:${typeKey(elem)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%arr.insertValues.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.insertValues", key); lowerer.arrHofHelpers.set(key, name); const outType = arrayOf(elem); const valueType = arrayValueType(lowerer, source.type.elem); @@ -1035,7 +1036,7 @@ export function lowerArrayFromCall( const key = `fromArray:${typeKey(arrT.elem)}:${typeKey(outElem)}:${typeKey(fnRet)}:${arity}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%arr.fromArray.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%arr.fromArray", key); lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push( buildArrayFromArrayFn(lowerer, helper, arrT.elem, outElem, fnRet, arity, loc), @@ -1122,7 +1123,7 @@ export function lowerArrayFromCall( const key = `fromLen:${typeKey(fnRet)}:${arity}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%arr.fromLen.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%arr.fromLen", key); lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push(buildArrayFromLenFn(helper, fnRet, arity, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index e9a130a2db..44182fbda6 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { liftedFreshSpread } from "../coercions/fresh-spread.js"; import { lowerArrayIteratorValue } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; @@ -1616,7 +1617,7 @@ function arraySearchHelper( const key = `${method}:${typeKey(elem)}:${typeKey(valueT)}:${typeKey(needleT)}:${directNeedle}:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); const resultT = method === "includes" ? BOOL : F64; @@ -1972,7 +1973,7 @@ export function lowerTupleReadMethodCall( let helper = lowerer.arrHofHelpers.get(key); const resultType = method === "includes" ? BOOL : F64; if (!helper) { - helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%tuple.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const params = [ { localId: "a.0", name: "a", type: receiver.type }, @@ -2034,7 +2035,7 @@ export function lowerTupleReadMethodCall( const key = `tuple.slice:${typeKey(receiver.type)}:${args.length}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%tuple.slice.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%tuple.slice", key); lowerer.arrHofHelpers.set(key, helper); const params = [receiver, ...args].map((arg, i) => ({ localId: i === 0 ? "a.0" : `arg.${i}`, @@ -2125,7 +2126,7 @@ export function lowerTupleReadMethodCall( const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(fnArg.type)}:${typeKey(outElem)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%tuple.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const out = varRef("out.0", outType, loc); const result = varRef("r.0", ret, loc); @@ -2253,7 +2254,7 @@ function lowerArrayAtCall( const key = `at:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(index.type)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%arr.at.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.at", key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index cf45a75751..180f404351 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js"; import { BOOL, @@ -851,7 +852,7 @@ class PipelineFusion { if (!built) return null; let name = this.helpers.get(built.key); if (!name) { - name = `%arr.fused.${this.helpers.size}`; + name = keyedHelperName(this.lowerer, "%arr.fused", built.key); this.helpers.set(built.key, name); this.added.push({ ...built.fn, name }); } diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 9d8def80ea..8a43647543 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, dynUndefinedExpr, numLit, varRef } from "../../../ir/build.js"; import { BUF_NUM_METHODS } from "./buffer-numeric-methods.js"; import * as ts from "../../ts7/adapter.js"; @@ -77,7 +78,7 @@ function lowerBytesSortCall( const cacheKey = `bytes.${method}:${bytesT.elem}:${key}`; let helper = lowerer.arrHofHelpers.get(cacheKey); if (!helper) { - helper = `%bytes.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%bytes.${method}`, cacheKey); lowerer.arrHofHelpers.set(cacheKey, helper); lowerer.liftedFns.push( buildBytesSortFn(helper, bytesT, arity, hasComparator, copyFirst, order, loc), @@ -270,7 +271,7 @@ function bytesCallbackHelper( const key = `bytes.${method}:${bytesT.elem}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%bytes.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%bytes.${method}`, key); lowerer.arrHofHelpers.set(key, name); const reduce = method === "reduce" || method === "reduceRight"; const backward = method === "reduceRight" || method === "findLast" || method === "findLastIndex"; diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index 18754f0881..f39c8e86c7 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; import { isJsSourceFile, locOf } from "../../program.js"; @@ -285,7 +286,7 @@ export function truthyFilterCallback( const key = `filterBoolean:${typeKey(valueT)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%arr.filterBoolean.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.filterBoolean", key); lowerer.arrHofHelpers.set(key, name); const value = varRef("v.0", valueT, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts index 0bf1268301..dae68d518a 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; @@ -104,7 +105,7 @@ export function lowerGroupByStaticCall( const key = `map.groupBy:${typeKey(items.type)}:${typeKey(f.type)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.groupBy.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.groupBy", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildGroupByFn(lowerer, helper, items.type, f.type, resultT, loc)); } @@ -141,7 +142,7 @@ export function lowerGroupByStaticCall( const key = `object.groupBy:${typeKey(items.type)}:${typeKey(f.type)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%object.groupBy.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%object.groupBy", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildGroupByFn(lowerer, helper, items.type, f.type, resultT, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index 370974c85c..d3169ec5cb 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { nodeThrowExpr, boolLit, countedFor, numLit, strLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -232,7 +233,7 @@ function lowerMapIterDrainCall( const key = `${method}:${typeKey(mapT.key)}:${typeKey(mapT.value)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.${method}.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%map.${method}`, key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildMapIterDrainFn(helper, mapT, method, elemT, tupleT, loc)); } @@ -348,7 +349,7 @@ export function mapEntriesArray(lowerer: Lowerer, receiver: IrExpr): IrExpr { const key = `entries:${typeKey(mapT.key)}:${typeKey(mapT.value)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.entries.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.entries", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push( buildMapIterDrainFn(helper, mapT, "entries", tupleT, tupleT, receiver.loc), @@ -519,7 +520,7 @@ export function lowerMapForEachCall( const key = `${typeKey(mapT.key)}:${typeKey(mapT.value)}:${arity}:${typeKey(fnRet)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.forEach.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.forEach", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildCollectionForEachFn(helper, mapT, arity, fnRet, loc)); } @@ -693,7 +694,7 @@ export function lowerSetForEachCall( const key = `${typeKey(setT.elem)}:${arity}:${typeKey(fnRet)}`; let helper = lowerer.setHofHelpers.get(key); if (!helper) { - helper = `%set.forEach.${lowerer.setHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%set.forEach", key); lowerer.setHofHelpers.set(key, helper); lowerer.liftedFns.push(buildCollectionForEachFn(helper, setT, arity, fnRet, loc)); } @@ -745,7 +746,7 @@ function lowerSetCombineCall( const key = `${name}:${typeKey(setT.elem)}:${typeKey(otherT)}`; let helper = lowerer.setHofHelpers.get(key); if (!helper) { - helper = `%set.${name}.${lowerer.setHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%set.${name}`, key); lowerer.setHofHelpers.set(key, helper); lowerer.liftedFns.push(buildSetCombineFn(helper, name, setT, otherT, loc)); } @@ -1357,7 +1358,7 @@ export function mapFromSeedValue( const key = `checked-seed:${typeKey(mapT)}`; let name = lowerer.mapHofHelpers.get(key); if (!name) { - name = `%map.checkedSeed.${lowerer.mapHofHelpers.size}`; + name = keyedHelperName(lowerer, "%map.checkedSeed", key); lowerer.mapHofHelpers.set(key, name); const source = varRef("source.0", DYN, loc); const entries = varRef("entries.0", DYN, loc); @@ -1457,7 +1458,7 @@ export function mapFromSeedValue( const key = `seed:${typeKey(mapT.key)}:${typeKey(mapT.value)}:${elem.shapeId}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.seed.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.seed", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildMapSeedFn(mapT, helper, seed.type, elem, kT, vT, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index efc173ffca..b3faf649eb 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import { objectEnumerationReceiver } from "../object-enumeration-receiver.js"; @@ -184,7 +185,7 @@ export function objectIterOverIndexShape( const key = `obj.${member}:ovf:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.${member}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%obj.${member}`, key); const outRef = varRef("out.0", resultT, loc); const rRef = varRef("r.0", argIr, loc); @@ -608,7 +609,7 @@ function lowerTypedObjectFromEntriesCall( const key = `obj.fromEntries:${argIr.elem.shapeId}:${resultT.shapeId}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.fromEntries.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.fromEntries", key); const tupleT = argIr.elem as IrType & { kind: "record" }; const tRef = varRef("t.0", tupleT, loc); @@ -712,7 +713,7 @@ function lowerFromEntriesStringRows( const key = `obj.fromEntries:strrows:${resultT.shapeId}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.fromEntries.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.fromEntries", key); const tRef = varRef("t.0", rowT, loc); const rowLen: IrExpr = { @@ -931,7 +932,7 @@ export function lowerRecordOvfCaptureHelper( const key = `ovf:${fromId}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%rec.capture.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%rec.capture", key); lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "record", shapeId: fromId }; const toT: IrType = { kind: "record", shapeId: toId }; @@ -1204,7 +1205,7 @@ export function lowerObjectAssignIndexShape( const key = `assign:${targetIr.shapeId}:${plan.fromId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%obj.assign.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%obj.assign", key); lowerer.valueHelpers.set(key, name); const toT: IrType = { kind: "record", shapeId: targetIr.shapeId }; const fromT: IrType = { kind: "record", shapeId: plan.fromId }; @@ -1474,7 +1475,7 @@ export function lowerIndexMergeHelper( .join(","); const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%rec.merge.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%rec.merge", key); lowerer.valueHelpers.set(key, name); const toT: IrType = { kind: "record", shapeId: toId }; const ksT = arrayOf(STRING); @@ -1805,7 +1806,7 @@ export function lowerEnvToPairsHelper( const key = `env.pairs:${shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%env.pairs.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%env.pairs", key); lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; const arrT = arrayOf(STRING); diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index 540518284d..ba5d0684da 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, numLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -966,7 +967,7 @@ function lowerFusedChain( const key = `iter:${srcKey}:${stageKeys.join(",")}:${terminal}:${termArgs.map((a) => typeKey(a.type)).join(",")}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%iter.${terminal}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%iter.${terminal}`, key); lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push( buildIterChainFn( @@ -1038,7 +1039,7 @@ function lowerHelperObject( const key = `iterHelper:${stage.kind}:${srcKey}:${typeKey(srcElem)}:${typeKey(arg.type)}:${typeKey(outT)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%iter.${stage.kind}.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, `%iter.${stage.kind}`, key); lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( buildHelperGenFn(lowerer, name, source, srcElem, stage, arg.type, outT, resultType, loc), @@ -1058,7 +1059,7 @@ function internBudgetChecker(lowerer: Lowerer, loc: SrcLoc, srcT: GenType | null const key = srcT === null ? "iter:budget" : `iter:budget:close:${typeKey(srcT)}`; let name = lowerer.arrHofHelpers.get(key); if (name !== undefined) return name; - name = srcT === null ? "%iter.budget" : `%iter.budget.close.${lowerer.arrHofHelpers.size}`; + name = srcT === null ? "%iter.budget" : keyedHelperName(lowerer, "%iter.budget.close", key); lowerer.arrHofHelpers.set(key, name); const v = (): IrExpr => ({ kind: "varRef", localId: "v.0", type: F64, loc }); diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 38865093fe..1947f0d75d 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { matchAllRowsReadByElements } from "./match-row-uses.js"; import { boolLit, @@ -159,7 +160,7 @@ export function lowerStringSplitCall( const key = `str.split:${receiver.type.kind === "union" ? typeKey(receiver.type) : receiver.type.kind}:${typeKey(separator.type)}:${typeKey(limit.type)}:${absent}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.split.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.split", key); const values = [receiver, separator, limit]; const params = values.map((value, index) => ({ localId: `arg.${index}`, @@ -395,7 +396,7 @@ function stringIndexHelperCall( const key = `str.index:${method}:${typeKey(receiver.type)}:${typeKey(index.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.index.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.index", key); lowerer.valueHelpers.set(key, helper); const rawReceiver = varRef("arg.0", receiver.type, loc); const rawIndex = varRef("arg.1", index.type, loc); @@ -673,7 +674,7 @@ export function lowerStringPaddingCall( const key = `str.pad:${method}:${values.map((value) => typeKey(value.type)).join(":")}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.pad.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.pad", key); const params = values.map((value, index) => ({ localId: `arg.${index}`, name: `arg${index}`, @@ -1309,7 +1310,7 @@ export function lowerStringMethodCall( const key = `str.positions:${entry.method}:${typeKey(needle.type)}:${typeKey(position.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.positions.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.positions", key); const params = [receiver, needle, position].map((arg, index) => ({ localId: `arg.${index}`, name: `arg${index}`, @@ -1411,7 +1412,7 @@ export function lowerStringMethodCall( const key = `str.positions:${entry.method}:${args.map((arg) => typeKey(arg.type)).join(":")}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.positions.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.positions", key); const params = [receiver, ...args].map((arg, index) => ({ localId: `arg.${index}`, name: `arg${index}`, diff --git a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts index 6856bc50bb..780c77d0f0 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { BOOL, @@ -162,7 +163,7 @@ function lowerStringReplacementValues( const key = `str.${method}:${typeKey(replacement.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.${method}.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%str.${method}`, key); lowerer.valueHelpers.set(key, helper); const subject = varRef("s.0", STRING, loc); const needle = varRef("search.0", STRING, loc); diff --git a/packages/compiler/src/frontend/lowering/dynamic-tier.ts b/packages/compiler/src/frontend/lowering/dynamic-tier.ts index 0293f9ab7c..3e0cea20a5 100644 --- a/packages/compiler/src/frontend/lowering/dynamic-tier.ts +++ b/packages/compiler/src/frontend/lowering/dynamic-tier.ts @@ -304,7 +304,18 @@ export function scanDynamicTier(lowerer: Lowerer, module: IrModule): DynamicTier }); } const rank = { loop: 0, "called-in-loop": 1, recursive: 2 } as const; - hot.sort((a, b) => rank[a.hot] - rank[b.hot] || b.loopOperations - a.loopOperations); + // Functions that rank alike list in source order: by the file's place in + // the module order, then by position. + const fileOrder = new Map(lowerer.moduleOrder.map((sf, index) => [sf.fileName, index])); + const fileRank = (file: string): number => fileOrder.get(file) ?? lowerer.moduleOrder.length; + hot.sort( + (a, b) => + rank[a.hot] - rank[b.hot] || + b.loopOperations - a.loopOperations || + fileRank(a.loc.file) - fileRank(b.loc.file) || + (a.loc.file < b.loc.file ? -1 : a.loc.file > b.loc.file ? 1 : 0) || + a.loc.start - b.loc.start, + ); return { hot, packages: [...packages] @@ -332,14 +343,15 @@ function boundName(lowerer: Lowerer, loc: SrcLoc): string | null { /** A function's name as the source spells it, without a module prefix * (`%%m144.Session.finish` → `Session.finish`, `m163.find` → `find`). A - * function expression (`%fn`) takes its own name, else the name it is - * bound to, else `anonymous function`. */ + * function expression (`%fn.`, with `_` when the expression is + * named) takes its own name, else the name it is bound to, else + * `anonymous function`. */ function displayName(lowerer: Lowerer, f: HotDynamicFunction): string { const plain = f.name .replace(/^%+/, "") .replace(/^m\d+\./, "") .replace(/^%+/, ""); - const lambda = /^fn\d+(?:_(.+))?$/.exec(plain); + const lambda = /^fn\.[0-9a-f]+(?:_(.+))?$/.exec(plain); if (lambda === null) return plain; return lambda[1] ?? boundName(lowerer, f.loc) ?? "anonymous function"; } diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index fafbbf695a..7e449f12b7 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -1,4 +1,5 @@ /* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { cleanFuncAdaptable } from "../coercions/function-adapters.js"; import { type Lowerer, jsFuncNameOf } from "../lowerer.js"; @@ -801,7 +802,7 @@ function nullProtoToString(lowerer: Lowerer, e: IrExpr, t: IrType & { kind: "rec const key = `nullProtoToString:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%rec.tostr.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%rec.tostr", key); lowerer.valueHelpers.set(key, helper); const r = varRef("r.0", t, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index a17af9b835..a2f399e346 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { dateJsonType, isDateJsonSource } from "../builtins/dates.js"; import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js"; import { pathModuleValue } from "../lower-builtin-values.js"; @@ -409,7 +410,7 @@ export function lowerDynObjectLiteral( if (!(err instanceof PoisonError) || !pureMember) throw err; const fence = lowerer.deferToRuntimeFence(propDiagsBefore, prop, { kind: "closure", - name: () => `%fn${lowerer.lambdaCounter++}_dynfence`, + name: () => lowerer.closureName(loc, "_dynfence"), returnType: VOID, type: { kind: "func", params: [], ret: VOID }, }); @@ -464,7 +465,7 @@ export function lowerDynObjectLiteral( if (!(err instanceof PoisonError)) throw err; const fence = lowerer.deferToRuntimeFence(convDiagsBefore, prop, { kind: "closure", - name: () => `%fn${lowerer.lambdaCounter++}_dynfence`, + name: () => lowerer.closureName(loc, "_dynfence"), returnType: VOID, type: { kind: "func", params: [], ret: VOID }, }); @@ -2959,7 +2960,7 @@ function lowerDeclaredSpreadMerge( const key = `declmerge-source:${type.shapeId}:${source.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%rec.declmerge.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%rec.declmerge", key); const sourceType = source.value.type; const destination = varRef("out.0", type, loc); const receiver = varRef("source.0", sourceType, loc); diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index fbcecdc328..4b8c5692a4 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -240,7 +240,7 @@ export function lowerOptionalChain( const receiver = lowerer.runtimeOptionalSourceValue(recvNode, loweredReceiver) ?? loweredReceiver; if (receiver.type.kind === "dyn") { if (dotNode === expr && ts.isCallExpression(expr)) { - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const callee: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; const stmts: IrStmt[] = []; let guarded = receiver; @@ -298,7 +298,7 @@ export function lowerOptionalChain( ); } if (dotNode === expr && ts.isElementAccessExpression(expr)) { - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const reference: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; lowerer.chainRecvByNode.set(recvNode, reference); lowerer.chainHandled.add(dotNode); @@ -331,7 +331,7 @@ export function lowerOptionalChain( dotNode === expr.expression ) { const m = dotNode.name.text; - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const recvRef: IrExpr = { kind: "chainRecv", id, type: DYN, loc: locOf(recvNode) }; lowerer.chainRecvByNode.set(recvNode, recvRef); lowerer.chainHandled.add(dotNode); @@ -358,7 +358,7 @@ export function lowerOptionalChain( } return { kind: "optChain", id, receiver, body, type: DYN, loc }; } - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const reference: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; lowerer.chainRecvByNode.set(recvNode, reference); lowerer.chainHandled.add(dotNode); @@ -420,7 +420,7 @@ export function lowerOptionalChain( rest.length === 1 ? rest[0]! : { kind: "union" as const, unionId: lowerer.unions.transform(def, rest) }; - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); // A single present arm binds its payload. Multiple present arms bind // the original tagged value, then retag inside the guarded body. The // nullish arms never enter that body, and each record keeps its identity. diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index ea075c528e..d661109509 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1,4 +1,5 @@ /* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */ +import { keyedHelperName } from "../stable-names.js"; import { staticallySelected } from "../static-typeof.js"; import * as ts from "../../ts7/adapter.js"; import { lowerHistogramProperty } from "../builtins/performance.js"; @@ -2793,7 +2794,7 @@ export function lowerGroupsProjection( const key = `regexgroups:${shapeId}:${groups.map((g) => `${g.name}=${g.index}`).join(",")}:${recvKey}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%regex.groups.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%regex.groups", key); lowerer.valueHelpers.set(key, helper); const body: IrStmt[] = []; const locals: IrLocal[] = [{ id: "m.0", name: "m", type: recv.type, mutable: true }]; diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 088b45cffa..8363918352 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -1,4 +1,5 @@ /* instanceof and in, including private-name and runtime-key in. */ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { builtinClassInstanceOfAny, builtinClassKind } from "../builtins/builtin-class-dyn.js"; import { @@ -647,7 +648,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const key = `instanceof.union:${unionId}:${target.def.name}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%instanceof.union.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%instanceof.union", key); lowerer.valueHelpers.set(key, helper); const value: IrExpr = { kind: "varRef", localId: "value.0", type: left.type, loc }; const body: IrStmt[] = []; @@ -1847,7 +1848,7 @@ function lowerRuntimeKeyIn( const hkey = `haskey:${recvT.shapeId}`; let helper = lowerer.valueHelpers.get(hkey); if (!helper) { - helper = `%rec.haskey.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%rec.haskey", hkey); lowerer.valueHelpers.set(hkey, helper); const recT: IrType = { kind: "record", shapeId: recvT.shapeId }; diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index e843b6909f..1fff26585a 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -1,4 +1,6 @@ import { staticNameBindings } from "./static-function-names.js"; +import { positionTag } from "./structural-positions.js"; +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import type { Lowerer } from "./lowerer.js"; @@ -233,7 +235,10 @@ export function collectGenericSignature(lowerer: Lowerer, decl: ts.FunctionDecla lowerer.genericFnsBySymbol.set(symbol, { decl, baseName: nameText, - qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText), + qualifiedName: lowerer.qualify( + ts.getSourceFileOfNode(decl), + nsPathPrefix(lowerer, decl) + nameText, + ), typeParams, instances: new Map(), }); @@ -774,7 +779,7 @@ function internGenericInstance( const typeArgsText = `<${rendered.length > 80 ? rendered.slice(0, 77) + "..." : rendered}>`; inst = { path, - name: `${info.qualifiedName}%${info.instances.size}`, + name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""), ordinal: info.instances.size, params, returnType, @@ -2252,7 +2257,7 @@ function internImplicitInstance( const declared = dynamicBroadPromise ? null : implicitDeclaredReturn(lowerer, info); inst = { path, - name: `${info.qualifiedName}%${info.instances.size}`, + name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""), ordinal: info.instances.size, params: shapes, // The promise callers rely on before the body settles it: the @@ -2440,7 +2445,7 @@ export function implicitLocalFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(stmt ?? decl, decl) + `${name}%l${ts.tokenStartOfNode(decl)}`, + nsPathPrefix(lowerer, stmt ?? decl, decl) + `${name}%l${positionTag(lowerer, decl)}`, ), typeParams: [], instances: new Map(), @@ -2536,7 +2541,7 @@ export function objLitGenericFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(fnNode), - `%ol${ts.tokenStartOfNode(fnNode)}.${name}`, + `%ol${positionTag(lowerer, fnNode)}.${name}`, ), typeParams, instances: new Map(), @@ -2682,7 +2687,7 @@ export function bindingGenericFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(stmt ?? decl, decl) + + nsPathPrefix(lowerer, stmt ?? decl, decl) + (topLevel ? name : `${name}%l${ts.tokenStartOfNode(decl)}`), ), typeParams, diff --git a/packages/compiler/src/frontend/lowering/intern-tables.ts b/packages/compiler/src/frontend/lowering/intern-tables.ts index fc69237db1..e3f2b08890 100644 --- a/packages/compiler/src/frontend/lowering/intern-tables.ts +++ b/packages/compiler/src/frontend/lowering/intern-tables.ts @@ -23,6 +23,7 @@ * The entry handles that itable.find and itable.insert answer stay inside * these lowerings: f64 locals that only reach toBool and itable.value. * Any other use of the class or its members as values is refused. */ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { BOOL, F64, STRING, funcOf, typeKey } from "../../ir/ir.js"; import type { IrExpr, IrFunction, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; @@ -447,7 +448,11 @@ function helperName(lowerer: Lowerer, member: InternTableMember, signature: stri } function registerHelper(lowerer: Lowerer, member: InternTableMember, signature: string): string { - const name = `%${INTERN_TABLE_CLASS}.${member}.${lowerer.internTableHelpers.size}`; + const name = keyedHelperName( + lowerer, + `%${INTERN_TABLE_CLASS}.${member}`, + `${member}|${signature}`, + ); lowerer.internTableHelpers.set(`${member}|${signature}`, name); return name; } diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index 898c46f205..e50652182e 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { BOOL, @@ -86,7 +87,7 @@ function dynIteratorAdapter( const key = `iterAdapt:${typeKey(expected)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%iter.adapt.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%iter.adapt", key); lowerer.valueHelpers.set(key, name); const locals: IrLocal[] = []; const local = (id: string, type: IrType, mutable = false): IrExpr => { @@ -313,7 +314,7 @@ export function lowerCollectionIteratorValue( loc, }; if (!typeEquals(yielded.type, type.yieldT)) return null; - name = `%coll.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%coll.iter", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -411,7 +412,7 @@ export function lowerBytesIteratorValue( const key = `bytesIter:${method}:${typeKey(bytesT)}:${typeKey(type)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%bytes.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%bytes.iter", key); lowerer.valueHelpers.set(key, name); const list = varRef("list.0", bytesT, loc); const index = varRef("index.0", F64, loc); @@ -517,7 +518,7 @@ export function lowerStringIteratorValue( const key = `strIter:${typeKey(type)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%str.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%str.iter", key); lowerer.valueHelpers.set(key, name); const text = varRef("text.0", STRING, loc); const index = varRef("index.0", F64, loc); @@ -677,7 +678,7 @@ function generatorRetypeAdapter( type: resumeType(expected.nextT), loc, }; - name = `%gen.retype.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%gen.retype", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -794,7 +795,7 @@ export function generatorDrain( const outRef = varRef("out.0", arrT, loc); const value = lowerer.coerceToExpected(genValue(genRef, loc), elem); if (!typeEquals(value.type, elem)) return null; - name = `%gen.drain.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%gen.drain", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -856,7 +857,7 @@ export function generatorDrain( if (!raw) return null; const value = lowerer.coerceToExpected(raw, elem); if (!typeEquals(value.type, elem)) return null; - name = `%gen.drain.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%gen.drain", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -978,7 +979,7 @@ export function lowerArrayIteratorValue( }; } if (!yielded || !typeEquals(yielded.type, type.yieldT)) return null; - name = `%arr.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.iter", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts index b9bbf62168..e27dd41837 100644 --- a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts +++ b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts @@ -21,6 +21,7 @@ * files only, and an empty answer leaves every mapping unchanged, so * TypeScript programs and JavaScript that never uses these patterns * compile exactly as before. */ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { isJsSourceFile, locOf } from "../program.js"; import { BOOL, DYN, type IrExpr, type IrStmt } from "../../ir/ir.js"; @@ -215,10 +216,10 @@ export function lowerValueReturningConstructorNew( "spread arguments when constructing a class whose constructor returns a value", ); } - const key = `ctorValue:${ts.getSourceFileOfNode(decl).fileName}:${ts.tokenStartOfNode(decl)}`; + const key = `ctorValue:${lowerer.positions.of(decl)}`; let name = lowerer.valueHelpers.get(key); if (name === undefined) { - name = `%ctorValue.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%ctorValue", key); lowerer.valueHelpers.set(key, name); const savedClass = lowerer.currentClass; lowerer.currentClass = null; diff --git a/packages/compiler/src/frontend/lowering/json-require.ts b/packages/compiler/src/frontend/lowering/json-require.ts index 9277452537..d6c2da571d 100644 --- a/packages/compiler/src/frontend/lowering/json-require.ts +++ b/packages/compiler/src/frontend/lowering/json-require.ts @@ -10,6 +10,7 @@ * JSON module is ordinary storage holding that object — destructuring * copies its properties at the require, exactly like Node. ES-module JSON * imports keep their own record (Node's ESM loader caches separately). */ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { PoisonError } from "./lowerer.js"; @@ -84,7 +85,7 @@ export function collectJsonRequires(lowerer: Lowerer, parts: FileParts[]): void ); throw new PoisonError(); } - const id = `%g.jsonRequire.${lowerer.jsonRequireModules.size}`; + const id = keyedHelperName(lowerer, "%g.jsonRequire", jsonSf.fileName); const global: IrGlobal = { id, name: "%json", type: mapped, mutable: true }; const loadedId = `${id}%loaded`; lowerer.globalsList.push(global, { diff --git a/packages/compiler/src/frontend/lowering/lifetime-regions.ts b/packages/compiler/src/frontend/lowering/lifetime-regions.ts index b212ae473f..b684c0395b 100644 --- a/packages/compiler/src/frontend/lowering/lifetime-regions.ts +++ b/packages/compiler/src/frontend/lowering/lifetime-regions.ts @@ -14,6 +14,7 @@ * Any other use of the class or its methods as values is refused. The * module is marked lifetimeRegions, which moves every ProgramLifetime * class into counted regions. */ +import { keyedHelperName } from "./stable-names.js"; import { F64, VOID, funcOf, typeKey } from "../../ir/ir.js"; import type { IrExpr, IrFunction, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js"; @@ -152,7 +153,7 @@ function runHelper(lowerer: Lowerer, fnType: IrType & { kind: "func" }, loc: Src const key = typeKey(fnType); const known = lowerer.regionRunHelpers.get(key); if (known !== undefined) return known; - const name = `%${LIFETIME_REGION_CLASS}.run.${lowerer.regionRunHelpers.size}`; + const name = keyedHelperName(lowerer, `%${LIFETIME_REGION_CLASS}.run`, key); lowerer.regionRunHelpers.set(key, name); const regionT = lifetimeRegionType(lowerer); const ret = fnType.ret; diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index 460f4d53a7..bc51a93826 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { InternalCompilerError } from "../../errors.js"; /* The node:assert lowering (a spoke module like lower-dgram.ts): the * callable module form (`assert(x)` through a default import or a CJS @@ -667,7 +668,7 @@ function partialEqHelper(lowerer: Lowerer, ta: IrType, tb: IrType, loc: SrcLoc): const key = `assert.pdeq:${typeKey(ta)}|${typeKey(tb)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.pdeq.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, "%assert.pdeq", key); lowerer.assertHelpers.set(key, name); const a = (): IrExpr => varRef("a.0", ta, loc); const b = (): IrExpr => varRef("b.0", tb, loc); @@ -1280,7 +1281,7 @@ function deepCheckCall( const key = `assert.dcheck:${variant}:${negated}:${typeKey(a.type)}:${typeKey(b.type)}`; let name = lowerer.assertHelpers.get(key); if (!name) { - name = `%assert.dcheck.${lowerer.assertHelpers.size}`; + name = keyedHelperName(lowerer, "%assert.dcheck", key); lowerer.assertHelpers.set(key, name); const pa = varRef("a.0", a.type, loc); const pb = varRef("b.0", b.type, loc); @@ -1844,7 +1845,7 @@ function assertThrowsHelper( const key = `assert.${mode}:${recvIsPromise ? "p" : "f"}:${typeKey(recvType)}:${sig}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.${mode}.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, `%assert.${mode}`, key); lowerer.assertHelpers.set(key, name); const caughtRef = (): IrExpr => varRef("e.0", CAUGHT, loc); @@ -2293,7 +2294,7 @@ function ifErrorHelper(lowerer: Lowerer, t: IrType & { kind: "union" }, loc: Src const key = `assert.iferr:${typeKey(t)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.iferr.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, "%assert.iferr", key); lowerer.assertHelpers.set(key, name); const def = lowerer.unions.get(t.unionId); if (!def) throw new InternalCompilerError(`assert.ifError over unknown union ${t.unionId}`); @@ -2464,7 +2465,7 @@ function deepEqHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc, loose = false): const key = `assert.${prefix}:${typeKey(t)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.${prefix}.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, `%assert.${prefix}`, key); lowerer.assertHelpers.set(key, name); const a = (): IrExpr => varRef("a.0", t, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index c503f0c9c8..c5e34306cd 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; import { lowerDynStreamMemberCall } from "./dyn-streams.js"; @@ -2095,7 +2096,7 @@ function lowerOptionalNumberDefault( const key = `number.optionalDefault:${widened.unionId}:${dflt}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%number.optionalDefault.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%number.optionalDefault", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", widened, loc); lowerer.liftedFns.push({ @@ -2204,7 +2205,7 @@ function primitiveConversionHandler( const key = `then.${name}:${typeKey(inner)}`; let fnName = lowerer.arrHofHelpers.get(key); if (fnName === undefined) { - fnName = `%then.${name}.${lowerer.arrHofHelpers.size}`; + fnName = keyedHelperName(lowerer, `%then.${name}`, key); lowerer.arrHofHelpers.set(key, fnName); const value = varRef("v.0", inner, loc); let body: IrExpr; @@ -2373,7 +2374,7 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc) const key = `bigint.scalar:${unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%bigint.scalar.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%bigint.scalar", key); lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); const body: IrStmt[] = []; @@ -3235,7 +3236,7 @@ function lowerScalarUnionNumber( const key = `number.scalar:${arg.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%number.scalar.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%number.scalar", key); lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); const nan: IrExpr = { @@ -3280,7 +3281,7 @@ function lowerOptionalStringNumber( const key = `${label}.optionalString:${arg.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%${label}.optionalString.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%${label}.optionalString`, key); lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); lowerer.liftedFns.push({ @@ -4707,7 +4708,7 @@ function lowerDefaultToStringCall( const key = `objToStr:${typeKey(recv.type)}${missing ? `:${missing}` : ""}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%obj.tostr.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.tostr", key); lowerer.valueHelpers.set(key, helper); const o = varRef("o.0", recv.type, loc); lowerer.liftedFns.push({ @@ -5394,7 +5395,7 @@ function loweredTemplateStrings( const site = lowerer.globalsList.length; const globalId = `%g.templateStrings.${site}`; const readyId = `%g.templateStringsReady.${site}`; - getter = `%templateStrings.get.${lowerer.valueHelpers.size}`; + getter = keyedHelperName(lowerer, "%templateStrings.get", key); lowerer.valueHelpers.set(key, getter); lowerer.globalsList.push( { id: globalId, name: "%templateStrings", type, mutable: true }, @@ -5575,7 +5576,7 @@ export function lowerPromiseMethodCall( // IS the spec's identity/thrower/no-op substitution for all three // members. An argument expression, when present, is undefined/null // by construction — nothing to evaluate. - const fnName = `%fn${lowerer.lambdaCounter++}_${member}pass`; + const fnName = lowerer.closureName(loc, `_${member}pass`); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT], ret: promT }; const fnCtx = newFnCtx(true, null, funcType, inner); fnCtx.isAsync = true; @@ -5672,7 +5673,7 @@ export function lowerPromiseMethodCall( // Evaluate both callback expressions before the wrapper runs, in source order. const fulfilled = fulfilledAbsent ? null : boxHandler(fulfilledNode, true); const rejected = rejectedAbsent ? null : boxHandler(rejectedNode, false); - const fnName = `%fn${lowerer.lambdaCounter++}_then2`; + const fnName = lowerer.closureName(loc, "_then2"); const paramTypes: IrType[] = [ promT, ...(fulfilled ? [fulfilled.value.type] : []), @@ -5921,7 +5922,7 @@ export function lowerPromiseMethodCall( ); }; const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: DYN }; - const fnName = `%fn${lowerer.lambdaCounter++}_then`; + const fnName = lowerer.closureName(loc, "_then"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, DYN], @@ -6016,7 +6017,7 @@ export function lowerPromiseMethodCall( ); } const R = resultT.inner; - const fnName = `%fn${lowerer.lambdaCounter++}_then`; + const fnName = lowerer.closureName(loc, "_then"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, cb.type], @@ -6119,7 +6120,7 @@ export function lowerPromiseMethodCall( "finally callbacks with parameters or a return value (use () => { ... })", ); } - const fnName = `%fn${lowerer.lambdaCounter++}_finally`; + const fnName = lowerer.closureName(loc, "_finally"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, cb.type], @@ -6222,7 +6223,7 @@ export function lowerPromiseMethodCall( } if (cb.type.kind === "dyn") { const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: DYN }; - const fnName = `%fn${lowerer.lambdaCounter++}_catchdyn`; + const fnName = lowerer.closureName(loc, "_catchdyn"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, DYN], @@ -6352,7 +6353,7 @@ export function lowerPromiseMethodCall( ); } const R = resultT.inner; - const fnName = `%fn${lowerer.lambdaCounter++}_catch`; + const fnName = lowerer.closureName(loc, "_catch"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT], ret: resultT }; const fnCtx = newFnCtx(true, null, funcType, R); fnCtx.isAsync = true; @@ -6589,7 +6590,7 @@ function filterNarrowHelper( const key = `filterNarrow:${test}:${typeKey(elem)}:${typeKey(outElem)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.filterNarrow.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.filterNarrow", key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); @@ -6835,7 +6836,7 @@ export function recordKeysArrayCall( const key = `obj.keys:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.keys.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.keys", key); const ref: IrExpr = { kind: "varRef", localId: "r.0", type: argIr, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: resultT, loc }; lowerer.arrHofHelpers.set(key, helper); @@ -6905,7 +6906,7 @@ function recordHasOwnHelper(lowerer: Lowerer, shapeId: string, loc: SrcLoc): str const key = `obj.hasOwn:${shapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = `%obj.hasOwn.${lowerer.arrHofHelpers.size}`; + const helper = keyedHelperName(lowerer, "%obj.hasOwn", key); lowerer.arrHofHelpers.set(key, helper); const shape = lowerer.shapes.get(shapeId)!; const recT: IrType = { kind: "record", shapeId }; @@ -6990,7 +6991,7 @@ function classAssignHelper( const key = `obj.assignClass:${className}:${srcShapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = `%obj.assignClass.${lowerer.arrHofHelpers.size}`; + const helper = keyedHelperName(lowerer, "%obj.assignClass", key); lowerer.arrHofHelpers.set(key, helper); const sShape = lowerer.shapes.get(srcShapeId)!; const oT: IrType = { kind: "object", className }; @@ -7058,7 +7059,7 @@ function recordAssignHelper( const key = `obj.assign:${targetShapeId}:${srcShapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = `%obj.assign.${lowerer.arrHofHelpers.size}`; + const helper = keyedHelperName(lowerer, "%obj.assign", key); lowerer.arrHofHelpers.set(key, helper); const sShape = lowerer.shapes.get(srcShapeId)!; const tShape = lowerer.shapes.get(targetShapeId)!; @@ -8515,7 +8516,7 @@ function lowerObjectStaticCall( const key = `obj.${member}:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.${member}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%obj.${member}`, key); const recT = argIr; const ref: IrExpr = { kind: "varRef", localId: "r.0", type: recT, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: resultT, loc }; @@ -9151,7 +9152,7 @@ function lowerInlineArrowBind( const length = firstDefault < 0 ? arrow.parameters.nodes.length : firstDefault; const key = `arrow.bind:${typeKey(type)}:${length}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%arrow.bind.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%arrow.bind", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); @@ -10080,7 +10081,7 @@ function lowerUnionObjectDynFieldCall( const receiverRef = varRef(receiverLocal.id, receiverT, loc); let helper = lowerer.unionCallHelpers.get(key); if (!helper) { - helper = `%union.call.${lowerer.unionCallHelpers.size}`; + helper = keyedHelperName(lowerer, "%union.call", key); lowerer.unionCallHelpers.set(key, helper); const receiverRef = varRef("this.0", receiverT, loc); const branch = (plan: (typeof plans)[number]): IrStmt[] => { @@ -10173,7 +10174,7 @@ function unionObjectMethodHelper( const key = `${receiverT.unionId}:${method}:${shapes.map((shape) => `${shape.mode}:${typeKey(shape.type)}`).join(",")}:${typeKey(resultT)}`; const existing = lowerer.unionCallHelpers.get(key); if (existing) return existing; - const name = `%union.call.${lowerer.unionCallHelpers.size}`; + const name = keyedHelperName(lowerer, "%union.call", key); lowerer.unionCallHelpers.set(key, name); // A probe that catches a refusal while the body lowers must not leave // the name registered for a helper that was never emitted. @@ -10367,7 +10368,7 @@ export function lowerStaticNumberGlobal( const key = `parseInt.optional:${unionT.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%parseInt.optional.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%parseInt.optional", key); lowerer.valueHelpers.set(key, helper); const value: IrExpr = { kind: "varRef", localId: "value.0", type: unionT, loc }; const radixRef: IrExpr = { kind: "varRef", localId: "radix.0", type: F64, loc }; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 7e6f80f2cb..4ea4583f68 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; import { adaptPromiseExecutor } from "./promise-executor-adapter.js"; import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; @@ -84,6 +85,7 @@ import { } from "./call-signatures.js"; import { omittedArgFor } from "./call-arguments.js"; import { + checkerTypeDescription, isGenericCallableMemberType, jsOpenObjectType, typeKey, @@ -4664,7 +4666,25 @@ export function genericClassInstanceType( ); } const ordinal = gci.instances.size; - const name = `${gci.family.def.name}%${ordinal}`; + // The name does not take the checker type ids of the key: a literal + // argument is named by its text, another checker type by its + // description. Two keys that still describe alike number the later one. + const nameKey = + mapped.map(typeKey).join(",") + + (gci.literalSensitive.size > 0 + ? `@${gci.typeParams + .map((_, i) => + sensitive(i) + ? (literalText(i) ?? checkerTypeDescription(tsArgs[i]!, lowerer.typeCtx)) + : "*", + ) + .join(",")}` + : ""); + const prefix = `${gci.family.def.name}%`; + const taken = new Set([...gci.instances.values()].map((instance) => instance.name)); + let name = keyedHelperName(lowerer, prefix, nameKey, ""); + for (let n = 1; taken.has(name); n++) + name = keyedHelperName(lowerer, prefix, `${nameKey}#${n}`, ""); const entry: { name: string; info: ClassInfo | null; poisoned?: boolean } = { name, info: null }; gci.instances.set(key, entry); const bindings = new Map(); @@ -7954,7 +7974,7 @@ export function classIteratorDrainCall( const key = `${cit.className}:${typeKey(outElem)}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { - name = `%iter.drain.${lowerer.iterDrainHelpers.size}`; + name = keyedHelperName(lowerer, "%iter.drain", key); lowerer.iterDrainHelpers.set(key, name); const recvT: IrType = { kind: "object", className: cit.className }; const recvRef: IrExpr = { kind: "varRef", localId: "r.0", type: recvT, loc }; @@ -8068,7 +8088,7 @@ export function classIteratorRestDrainCall( const key = `it:${cit.iterT.className}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { - name = `%iter.drain.${lowerer.iterDrainHelpers.size}`; + name = keyedHelperName(lowerer, "%iter.drain", key); lowerer.iterDrainHelpers.set(key, name); const itRef: IrExpr = { kind: "varRef", localId: "it.0", type: cit.iterT, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: outT, loc }; @@ -10576,7 +10596,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const key = `x509.record:${isBytes ? "bytes" : "str"}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%x509.record.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%x509.record", key); lowerer.valueHelpers.set(key, helper); const dataT = data.type; const dRef: IrExpr = { kind: "varRef", localId: "d.0", type: dataT, loc }; diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index 5c99ebc821..aa90fc7c74 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, boolLit } from "../../ir/build.js"; /* The dgram/dns-surface lowering (node:dgram + node:dns — a spoke module * like lower-server.ts): module-function calls (dgram createSocket, dns @@ -357,7 +358,7 @@ function checkedDynPromise( const key = `dns.promise:${typeKey(declared)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%dns.promise.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%dns.promise", key); lowerer.valueHelpers.set(key, helper); const promiseT: IrType = { kind: "promise", inner: DYN }; lowerer.liftedFns.push({ @@ -891,7 +892,7 @@ export function lowerPromisifiedDnsCall( const key = `dnsPromise:${JSON.stringify(value)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%dnsPromise.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%dnsPromise", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index f472672b20..f105af786d 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -42,6 +42,7 @@ * emit-overrides block at the bottom of this file (per-event * monomorphization of the override body, dispatch on the dynamic class, * super.emit as the prototype chain). */ +import { keyedHelperName } from "./stable-names.js"; import { checkKeypressListener } from "./builtins/readline.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; @@ -631,7 +632,7 @@ function lowerFlexListenerCall( const key = `emitter.${registering ? "onFlex" : "offDyn"}:${typeKey(recvT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.${registering ? "onFlex" : "offFlex"}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%emitter.${registering ? "onFlex" : "offFlex"}`, key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -850,7 +851,7 @@ function lowerDynListenerCall( const existing = lowerer.arrHofHelpers.get(key); let helper = existing; if (!helper) { - helper = `%emitter.${registering ? "onDyn" : "offDyn"}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%emitter.${registering ? "onDyn" : "offDyn"}`, key); lowerer.arrHofHelpers.set(key, helper); const check: IrStmt = { kind: "exprStmt", @@ -1000,7 +1001,7 @@ export function lowerDynamicEmitterEmit( const key = `emitter.dynEmit:${typeKey(recvT)}:${k}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.dynEmit.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%emitter.dynEmit", key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -1092,7 +1093,7 @@ export function lowerDynamicEmitterOn( const key = `emitter.dynOn:${typeKey(recvT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.dynOn.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%emitter.dynOn", key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -2359,7 +2360,7 @@ function emitDispatchExpr( const key = `emitter.emitDispatch:${typeKey(recvT)}:${name}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.emitDispatch.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%emitter.emitDispatch", key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -2424,7 +2425,7 @@ function eventsOnceBoxer( const key = `eventsOnce:${typeKey(fnType)}`; let name = lowerer.valueHelpers.get(key); if (name === undefined) { - name = `%events.onceArgs.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%events.onceArgs", key); lowerer.valueHelpers.set(key, name); const params: IrParam[] = tuple.map((type, i) => ({ localId: `%once.a${i}`, diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index e7a1b79a83..ba96bea8ca 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; @@ -2274,7 +2275,11 @@ export function cjsAccessorSnapshotOf( if (!lexerVisibleAccessors(ts.getSourceFileOfNode(getter)).includes(getter)) return null; const call = cjsAccessorCall(lowerer, getter, locOf(getter)); if (call === null) return null; - const snapshot = { id: `%g.cjsSnapshot.${lowerer.cjsAccessorSnapshots.size}`, type: call.type }; + const at = locOf(getter); + const snapshot = { + id: keyedHelperName(lowerer, "%g.cjsSnapshot", `${at.file}:${at.start}:${at.end}`), + type: call.type, + }; lowerer.globalsList.push({ id: snapshot.id, name: "%cjsSnapshot", @@ -2299,7 +2304,7 @@ export function cjsAccessorSnapshotStores( if (getters.length === 0) return []; let readyId = lowerer.cjsSnapshotReady.get(dep); if (readyId === undefined) { - readyId = `%g.cjsSnapshotReady.${lowerer.cjsSnapshotReady.size}`; + readyId = keyedHelperName(lowerer, "%g.cjsSnapshotReady", dep.fileName); lowerer.globalsList.push({ id: readyId, name: "%cjsSnapshotReady", type: BOOL, mutable: true }); lowerer.cjsSnapshotReady.set(dep, readyId); } diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index cd8dc31d1d..653c0d837e 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -270,7 +270,7 @@ export function lowerLambda( const nameIdent = !ts.isArrowFunction(node) && node.name && ts.isIdentifier(node.name) ? node.name : null; const baseName = nameIdent ? nameIdent.text : ""; - const fnName = `%fn${lowerer.lambdaCounter++}${baseName ? `_${baseName}` : ""}`; + const fnName = lowerer.closureName(locOf(node), baseName ? `_${baseName}` : ""); // Named function expressions/declarations can self-reference by name; an // object-literal method's name is a PROPERTY, not a binding — no self. const selfSymbol = diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 4be5d0b7ef..1979b43aa8 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, boolLit, @@ -268,7 +269,7 @@ function errorInspectHelper( const key = `insp.userError:${typeKey(t)}`; const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; - const name = `%util.insp.${lowerer.inspectHelpers.size}`; + const name = keyedHelperName(lowerer, "%util.insp", key); lowerer.inspectHelpers.set(key, name); const v = (): IrExpr => varRef("v.0", t, loc); const r = (): IrExpr => varRef("r.0", F64, loc); @@ -1145,7 +1146,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { const key = `insp:${typeKey(t)}`; const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; - const name = `%util.insp.${lowerer.inspectHelpers.size}`; + const name = keyedHelperName(lowerer, "%util.insp", key); lowerer.inspectHelpers.set(key, name); const v = (): IrExpr => varRef("v.0", t, loc); @@ -2074,7 +2075,7 @@ function formatUnionHelper( const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; const loc = helperLoc(site); - const name = `%util.fmtv.${lowerer.inspectHelpers.size}`; + const name = keyedHelperName(lowerer, "%util.fmtv", key); lowerer.inspectHelpers.set(key, name); const def = lowerer.unions.get(t.unionId); if (!def) throw new InternalCompilerError(`format value over unknown union ${t.unionId}`); diff --git a/packages/compiler/src/frontend/lowering/lower-mixins.ts b/packages/compiler/src/frontend/lowering/lower-mixins.ts index 766059dfb9..9de027563f 100644 --- a/packages/compiler/src/frontend/lowering/lower-mixins.ts +++ b/packages/compiler/src/frontend/lowering/lower-mixins.ts @@ -34,6 +34,7 @@ * statics-bearing mixin classes outside a plain top-level statement * (their declaration-time code must run exactly where the call * evaluates). */ +import { positionTag } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { PoisonError } from "./lowerer.js"; @@ -452,7 +453,7 @@ function instantiateMixinCall( const base = mixinBaseClassOf(lowerer, call.arguments.nodes[0]!); const instName = lowerer.qualify( ts.getSourceFileOfNode(call), - `%mx${ts.tokenStartOfNode(call)}.${shape.classNode.name?.text ?? ""}`, + `%mx${positionTag(lowerer, call)}.${shape.classNode.name?.text ?? ""}`, ); const ordinal = lowerer.mixinOrdinals.get(shape.classNode) ?? 0; lowerer.mixinOrdinals.set(shape.classNode, ordinal + 1); diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts index fcc68c787a..8c50dee460 100644 --- a/packages/compiler/src/frontend/lowering/lower-mock.ts +++ b/packages/compiler/src/frontend/lowering/lower-mock.ts @@ -17,6 +17,7 @@ * writes the original back on restore(). Each mock registers a closure * that calls its restore() with its tracker (scr_test.c): the module's * `mock`, or the test's t.mock, which is reset when the test ends. */ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { BOOL, @@ -136,7 +137,7 @@ function liftOnce( const key = `mock.${role}:${owner}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%mock.${role}.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, `%mock.${role}`, key); lowerer.assertHelpers.set(key, name); build(name); return name; diff --git a/packages/compiler/src/frontend/lowering/lower-modules.test.ts b/packages/compiler/src/frontend/lowering/lower-modules.test.ts index 84cc96e7dd..f1021e4da8 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.test.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.test.ts @@ -34,7 +34,7 @@ test("retains dependencies from function storage, captures, generator channels a const completed = record("completed"); const body = record("body"); const global = record("global", record("nested")); - const wanted = lowerer.shapes.shapes.map((shape) => shape.id); + const wanted = lowerer.shapes.shapes.map((shape) => shape.id).sort(); record("unused"); const unionId = lowerer.unions.intern([body, UNDEFINED_T]); const unusedUnion = lowerer.unions.intern([F64, UNDEFINED_T]); @@ -162,7 +162,7 @@ test("closes nested structural types and index-signature values", () => { sent = record("sent"), indexed = record("indexed"); const containerId = lowerer.shapes.intern([], false, indexed); - const wanted = lowerer.shapes.shapes.map((shape) => shape.id); + const wanted = lowerer.shapes.shapes.map((shape) => shape.id).sort(); record("unused"); const nested: IrType = { kind: "func", diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index d1f276d44b..0b99e88d3e 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -1,4 +1,5 @@ import { THREADS_MODULE } from "../threads-import.js"; +import { positionTag } from "./structural-positions.js"; import { EXPERIMENTAL_MODULE } from "../experimental-import.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { everyStmtList, everyTypeChild } from "../../ir/traverse.js"; @@ -1074,8 +1075,14 @@ export function moduleArtifacts( else delete rest.abstractMethods; return rest; }), - records: lowerer.shapes.shapes.filter((r) => shapeIds.has(r.id)), - unions: lowerer.unions.unions.filter((u) => unionIds.has(u.id)), + // Ids come from what each shape and union is (keyedHelperName), so + // ordering by id gives every lowering order the same module. + records: lowerer.shapes.shapes + .filter((r) => shapeIds.has(r.id)) + .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), + unions: lowerer.unions.unions + .filter((u) => unionIds.has(u.id)) + .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), }; } @@ -1677,7 +1684,7 @@ export function collectGlobals( } if (type.kind === "void") lowerer.badType(stmt.name, lowerer.typeOf(stmt.name)); const g: IrGlobal = { - id: `%g.${tag}${nsPathPrefix(stmt)}%alias.${stmt.name.text}%${ts.tokenStartOfNode(stmt)}`, + id: `%g.${tag}${nsPathPrefix(lowerer, stmt)}%alias.${stmt.name.text}%${positionTag(lowerer, stmt)}`, name: stmt.name.text, type, mutable: false, @@ -1754,7 +1761,7 @@ export function collectGlobals( // (`%g.e.A%B.x`), so `namespace A { export const x }` and a // top-level `const x` never collide; non-exported members add the // declaring block's position (block-local bindings). - const nsPrefix = nsPathPrefix(stmt, list.declarations.nodes[0]); + const nsPrefix = nsPathPrefix(lowerer, stmt, list.declarations.nodes[0]); for (const decl of list.declarations.nodes) { // --provenance-sources: an elided pure-annotated dead const in a // fetched source module registers no global (the statement diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index dca835d779..19123dae3d 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -36,6 +36,7 @@ * of the compiler already takes (a body running before the declaring * statement is not modeled — same as top-level const reads through early * calls). */ +import { positionTag, type PositionTagOwner } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import { packageShimFor } from "../package-shims.js"; import type { Lowerer } from "./lowerer.js"; @@ -164,7 +165,11 @@ function registerTypeOnlyBlocks(lowerer: Lowerer, decl: ts.ModuleDeclaration): v * distinct (resolution is by symbol; names only need uniqueness and * determinism). `modifierDecl` carries the declaration whose modifiers * decide exportedness (the first declarator of a variable statement). */ -export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string { +export function nsPathPrefix( + owner: PositionTagOwner, + node: ts.Node, + modifierDecl?: ts.Node, +): string { const parent = node.parent; if (parent === undefined || !ts.isModuleBlock(parent)) return ""; const flagsHost = modifierDecl ?? node; @@ -179,10 +184,10 @@ export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string { const nested = p.parent !== undefined && ts.isModuleBlock(p.parent); const nonExported = nested && (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Export) === 0; - segs.unshift(p.name.text + (nonExported ? `%${ts.tokenStartOfNode(p)}` : "")); + segs.unshift(p.name.text + (nonExported ? `%${positionTag(owner, p)}` : "")); } } - return `${segs.join("%")}${exported ? "" : `%${ts.tokenStartOfNode(parent)}`}.`; + return `${segs.join("%")}${exported ? "" : `%${positionTag(owner, parent)}`}.`; } /** The registered namespace-block kind a symbol's declaration sits in diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 058d7874aa..94d49d8b37 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, nodeThrowExpr, @@ -183,7 +184,7 @@ export function voidizedCallback(lowerer: Lowerer, cb: IrExpr, loc: SrcLoc): IrE const toT = funcOf([...fromT.params], VOID); const key = `server.voidcb:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%server.voidcb.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%server.voidcb", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; @@ -279,7 +280,7 @@ function receiverReturningCall( ): IrExpr { const key = `server.recvret:${fn}:${opts?.prefix?.fn ?? ""}:${(opts?.mainArgIndices ?? []).join(".")}:${callArgs.map((a) => typeKey(a.type)).join(",")}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%server.recvret.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%server.recvret", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const params = callArgs.map((a, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: a.type })); @@ -590,7 +591,7 @@ function h2HeadersCallbackAdapter( ); const key = `h2.hdrcb:${handleKind}:${withStatus}:${typeKey(fromT)}:${shapeId ?? ""}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%h2.hdrcb.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%h2.hdrcb", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; @@ -3583,7 +3584,7 @@ export function lowerCompatReqStreamOptionalCall( type: unionT, loc, }; - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const body = lowerH2StreamMethodCall(lowerer, call, call.expression, { kind: "chainRecv", id, @@ -4424,7 +4425,7 @@ function lowerHttpCreateServerForms( const hasMax = opts.maxHeaderSize !== null; const key = `server.opts:${opts.joinDup ? 1 : 0}:${opts.allowMissingHost ? 1 : 0}:${hasBuffer ? 1 : 0}:${hasMax ? 1 : 0}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%server.opts.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%server.opts", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const params = [ @@ -8009,7 +8010,7 @@ function lowerHttpResMethodCall( const all = [...callArgs, cb]; const key = `server.endcb:${fn}:${all.map((a) => typeKey(a.type)).join(",")}`; const existing = lowerer.arrHofHelpers.get(key); - const helper = existing ?? `%server.endcb.${lowerer.arrHofHelpers.size}`; + const helper = existing ?? keyedHelperName(lowerer, "%server.endcb", key); if (!existing) { lowerer.arrHofHelpers.set(key, helper); const params = all.map((a, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: a.type })); diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 2ad3657c7b..0beb24a0ea 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -2278,7 +2278,7 @@ function lowerStaticNamespaceImport( ): IrExpr { const loc = locOf(call); const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: ns }; - const fnName = `%fn${lowerer.lambdaCounter++}_staticImport`; + const fnName = lowerer.closureName(loc, "_staticImport"); const fnType: IrType & { kind: "func" } = { kind: "func", params: [], ret: resultT }; const fnCtx = newFnCtx(true, null, fnType, ns); fnCtx.isAsync = true; diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 33c842f6da..d6f89dc11e 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { nodeThrowExpr, boolLit, numLit, strLit } from "../../ir/build.js"; import { isValueStream, valueStreamOf } from "./builtins/hash-stream.js"; import { InternalCompilerError } from "../../errors.js"; @@ -315,7 +316,7 @@ function lowerStreamCallback( ); } } - const fnName = `%fn${lowerer.lambdaCounter++}_${which}cb`; + const fnName = lowerer.closureName(loc, `_${which}cb`); const fnCtx = newFnCtx(true, null, funcType, funcType.ret); lowerer.fnStack.push(fnCtx); try { @@ -1148,7 +1149,7 @@ function streamMethodWrapper( paramTypes.push(got.type); } const ret = found.sig.ret; - const fnName = `%fn${lowerer.lambdaCounter++}_${which}vt`; + const fnName = lowerer.closureName(loc, `_${which}vt`); const funcType = funcOf([thisType, ...paramTypes], ret); lowerer.fnStack.push(newFnCtx(true, null, funcType, ret)); try { @@ -1694,7 +1695,7 @@ function fromGenHelpers( const key = `streamFromGen:${role}:${typeKey(gen)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.fromGen.${role}.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, `%stream.fromGen.${role}`, key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -1783,7 +1784,7 @@ function classIteratorObject( const key = `streamFromIter:${cit.iterT.className}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.fromIter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.fromIter", key); lowerer.valueHelpers.set(key, name); const nextName = `${name}.next`; const nextT = funcOf([], DYN); @@ -1944,7 +1945,7 @@ function classAsyncIteratorObject( const key = `streamFromAsyncIter:${cit.className}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.fromAsyncIter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.fromAsyncIter", key); lowerer.valueHelpers.set(key, name); const it: IrLocal = { id: "it.0", name: "it", type: cit.iterT, mutable: false, boxed: true }; const src: IrLocal = { @@ -3128,7 +3129,7 @@ function pipelineBoxHelper( const key = `pipelineBox:${typeKey(promiseT)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.pipelineBox.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.pipelineBox", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -3155,7 +3156,7 @@ function pipelineAbortHelper(lowerer: Lowerer, ac: IrExpr, loc: SrcLoc): IrExpr let name = lowerer.valueHelpers.get(key); const abortT = funcOf([], VOID); if (!name) { - name = `%stream.pipelineAbort.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.pipelineAbort", key); lowerer.valueHelpers.set(key, name); const acLocal: IrLocal = { id: "ac.0", name: "ac", type: DYN, mutable: false, boxed: true }; const acRef = varRef(acLocal.id, DYN, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index 7d4bba34b6..03c9c5c1f2 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; import { isJsSourceFile } from "../program.js"; @@ -55,7 +56,7 @@ export function adaptZeroArgTimerCallback( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.dropret:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%timer.dropret.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%timer.dropret", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( @@ -117,7 +118,7 @@ export function adaptZeroArgTimerCallback( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.droparg:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%timer.droparg.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%timer.droparg", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( @@ -203,7 +204,7 @@ function makeTimerArgsThunk( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.argsthunk:${n}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%timer.argsthunk.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%timer.argsthunk", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; diff --git a/packages/compiler/src/frontend/lowering/lowerer.test.ts b/packages/compiler/src/frontend/lowering/lowerer.test.ts index bacdc7941c..7fbb9661fc 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.test.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.test.ts @@ -10,6 +10,7 @@ import { } from "../../ir/ir.js"; import type { Node, Program, SourceFile, Symbol, Type } from "../ts7/adapter.js"; import { Lowerer } from "./lowerer.js"; +import { UnionRegistry } from "../type-mapper.js"; import { classMethodValue } from "./class-method-values.js"; const loc = { file: "island.ts", start: 0, end: 1 }; @@ -32,7 +33,7 @@ test("reuses complete copy routes without repeating discriminator planning", () expect(plan).not.toHaveBeenCalled(); expect(lowerer.liftedFns.filter((fn) => fn.name === helper)).toHaveLength(1); const recursive = {} as Type; - lowerer.shapes.recursiveRef(recursive); + lowerer.shapes.recursiveRef(recursive, () => "Recursive"); lowerer.shapes.finalizeRecursive(recursive, [{ name: "kind", type: STRING }]); expect(lowerer.unionRetagHelper(from, to, loc)).toBe(helper); expect(plan).toHaveBeenCalled(); @@ -54,7 +55,7 @@ test("replans after a recursive union gains its final arms", () => { const lowerer = context(); const recursive = {} as Type; const from = lowerer.unions.intern([UNDEFINED_T, NULL_T]); - const to = lowerer.unions.recursiveRef(recursive); + const to = lowerer.unions.recursiveRef(recursive, () => "Recursive"); const trapped = lowerer.unionRetagHelper(from, to, loc); expect(trapped).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [UNDEFINED_T]); @@ -75,7 +76,7 @@ test("rechecks width conversions after recursive record definitions settle", () const lowerer = context(); const recursive = {} as Type; const source = record(lowerer.shapes.intern([{ name: "value", type: F64 }])); - const target = record(lowerer.shapes.recursiveRef(recursive)); + const target = record(lowerer.shapes.recursiveRef(recursive, () => "Recursive")); const from = lowerer.unions.intern([source, UNDEFINED_T]); const to = lowerer.unions.intern([target, NULL_T]); expect(lowerer.unionRetagHelper(from, to, loc)).not.toBeNull(); @@ -129,7 +130,7 @@ test("literal ownership is reused across values and rechecked when recursive rec expect(inspect).not.toHaveBeenCalled(); const recursive = {} as Type; - const pendingShape = record(lowerer.shapes.recursiveRef(recursive)); + const pendingShape = record(lowerer.shapes.recursiveRef(recursive, () => "Recursive")); const pending = lowerer.unions.intern([pendingShape], { field: "kind", cases: [{ tag: 0, values: ["a"] }], @@ -143,7 +144,7 @@ test("literal ownership is reused across values and rechecked when recursive rec test("literal ownership sees a recursive union's finalized discriminator", () => { const lowerer = context(); const recursive = {} as Type; - const union = lowerer.unions.recursiveRef(recursive); + const union = lowerer.unions.recursiveRef(recursive, () => "Recursive"); const shape = record(lowerer.shapes.intern([{ name: "kind", type: STRING }])); expect(lowerer.literalUnionArm(union, ["ready"])).toBeNull(); lowerer.unions.finalizeRecursive(recursive, [shape], { @@ -182,7 +183,10 @@ test("removing undefined reuses the discriminator transformation without hiding ); const recursive = {} as Type; - const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; + const pending: IrType = { + kind: "union", + unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), + }; expect(lowerer.stripUndefinedArm(pending)).toEqual(pending); lowerer.unions.finalizeRecursive(recursive, [STRING, UNDEFINED_T]); expect(lowerer.stripUndefinedArm(pending)).toEqual(STRING); @@ -215,7 +219,10 @@ test("adding undefined reuses semantic union transformations and sees recursive expect(lowerer.unions.get(added.unionId)?.discriminant).toEqual(discriminant); const recursive = {} as Type; - const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; + const pending: IrType = { + kind: "union", + unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), + }; const before = lowerer.withUndefinedArmOf(pending); lowerer.unions.finalizeRecursive(recursive, [a, b], discriminant); expect(lowerer.withUndefinedArmOf(pending)).toEqual(added); @@ -227,13 +234,18 @@ test("adding undefined reuses semantic union transformations and sees recursive test("optional union widening keeps refused and missing contracts current", () => { const lowerer = context(); const recursive = {} as Type; - const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; + const pending: IrType = { + kind: "union", + unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), + }; expect(lowerer.withUndefinedArmOf(pending)).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [{ kind: "date" }, STRING]); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); - const missing: IrType = { kind: "union", unionId: `u${lowerer.unions.unions.length}` }; + // Union ids come from their arms, so a fresh registry names the union + // this one has not interned yet. + const missing: IrType = { kind: "union", unionId: new UnionRegistry().intern([F64, STRING]) }; expect(lowerer.withUndefinedArmOf(missing)).toBeNull(); expect(lowerer.unions.intern([F64, STRING])).toBe(missing.unionId); expect(lowerer.withUndefinedArmOf(missing)?.kind).toBe("union"); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ac68fc972a..d8b86b59a2 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,4 +1,6 @@ import { relieveHeapPressure } from "../../heap-pressure.js"; +import { keyedHelperName } from "./stable-names.js"; +import { positionTag, StructuralPositions } from "./structural-positions.js"; import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js"; import { needsGeneralNodeStreams, @@ -1004,6 +1006,11 @@ function lowerShuffle(): ((wave: string[]) => void) | null { * the bodies reachable emit did NOT mark (plus deferred collection * diagnostics nothing flushed), reported separately: whole-program analysis without * letting unreached code fail builds. */ +/** The closure root of an emit-override specialization body. */ +function emitSpecRoot(spec: EmitSpecRequest): string { + return `%emit.${spec.info.def.name}.${spec.event}.${spec.ordinal}`; +} + export function lowerToIr( program: ts.Program, entry: ts.SourceFile, @@ -2807,6 +2814,8 @@ export class Lowerer { /** Generated value helpers, keyed by operation and type. Callers own * their key prefixes; this includes more than structural conversions. */ readonly valueHelpers = new Map(); + /** Each name keyedHelperName made, with a second hash of its key. */ + readonly keyedNameKeys = new Map(); /** structuralNarrowing's assignable classes, keyed by static and * narrowed class name. */ readonly structuralClassPeers = new Map(); @@ -2943,7 +2952,6 @@ export class Lowerer { ctx: FnCtx; } >(); - chainCounter = 0; /** Keyed by program-wide qualified class name (what IR object types carry). */ readonly classes = new Map(); readonly classBySymbol = new Map(); @@ -3173,7 +3181,53 @@ export class Lowerer { * dispatch revision needs no weak-key identity or process-global state. */ errorMethodDispatchRevision: string | null = null; errorPropertyDispatchRevision: string | null = null; - lambdaCounter = 0; + /** Structural source positions (structural-positions.ts) for names that + * stay the same when unrelated code moves. */ + /** Default library files are named by their base name: two checkers + * install them in different directories. */ + readonly positions = new StructuralPositions((sf) => + this.program.isSourceFileDefaultLibrary(sf) + ? `lib:${sf.fileName.slice(sf.fileName.lastIndexOf("/") + 1)}` + : sf.fileName, + ); + + /** The outermost node whose token span is exactly `loc`, or null. */ + nodeAtLoc(loc: SrcLoc): ts.Node | null { + const sf = this.program.getSourceFile(loc.file); + return sf === undefined ? null : (ts.nodeAtSpan(sf, loc.start, loc.end) ?? null); + } + + /** The lowering unit whose name scopes the closures it lifts. */ + private closureRoot = "%pre"; + private readonly closureNameCounts = new Map(); + + /** Runs `fn` with the closures it lifts named under `root`. */ + withClosureRoot(root: string, fn: () => T): T { + const previous = this.closureRoot; + this.closureRoot = root; + try { + return fn(); + } finally { + this.closureRoot = previous; + } + } + + /** The name of a closure (or wrapper) lifted for the source at `loc`: + * `.` of the current lowering unit, the location and + * `suffix`, then `suffix`. A unit that lifts several functions for the same + * location and suffix numbers them in the order it lifts them, so a + * closure's name depends only on its unit, not on which units lowered + * before it. */ + closureName(loc: SrcLoc, suffix = "", prefix = "%fn"): string { + const node = this.nodeAtLoc(loc); + const where = node === null ? `${loc.file}:${loc.start}:${loc.end}` : this.positions.of(node); + const key = `${prefix}|${this.closureRoot}|${where}|${suffix}`; + const first = keyedHelperName(this, prefix, key); + const seen = this.closureNameCounts.get(first) ?? 0; + this.closureNameCounts.set(first, seen + 1); + return (seen === 0 ? first : keyedHelperName(this, prefix, `${key}|${seen}`)) + suffix; + } + readonly preparedClassFactories = new Map(); readonly preparingClassFactories = new Set(); @@ -3526,6 +3580,7 @@ export class Lowerer { this.checker = program.getTypeChecker(); this.typeCtx = { checker: this.checker, + positionOf: (node) => this.positions.of(node), shapes: this.shapes, unions: this.unions, classNamer: this.classNamer, @@ -3818,11 +3873,11 @@ export class Lowerer { })() ? this.qualify( ts.getSourceFileOfNode(decl), - `%cx${ts.tokenStartOfNode(decl)}.${decl.name?.text ?? ""}`, + `%cx${positionTag(this, decl)}.${decl.name?.text ?? ""}`, ) : this.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(decl) + (decl.name ? decl.name.text : "%anon"), + nsPathPrefix(this, decl) + (decl.name ? decl.name.text : "%anon"), ); for (let index = this.localClassInstantiations.length - 1; index >= 0; index--) { const entry = this.localClassInstantiations[index]!; @@ -7381,7 +7436,10 @@ export class Lowerer { } for (const decl of fp.classDecls) { const info = this.classes.get(this.classNamer(decl)); - if (info) functions.push(...this.lowerClassMembers(info)); + if (info) + functions.push( + ...this.withClosureRoot(info.def.name, () => this.lowerClassMembers(info)), + ); else if (this.countsSkips()) this.countSkippedBody(decl); } } @@ -7394,7 +7452,10 @@ export class Lowerer { // are reachable by definition, already counted by reachable emit. if (!this.remainder) { for (const fp of parts) { - functions.push(this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!)); + const initName = this.initNameOf.get(fp.sf)!; + functions.push( + this.withClosureRoot(initName, () => this.lowerFileInit(fp.sf, fp.topStmts, initName)), + ); } functions.push(this.buildMain()); } @@ -7436,7 +7497,9 @@ export class Lowerer { // default): the diagnostic is recorded — the instance skips // like a signature-blocked function (lowerFunction's rule). try { - functions.push(this.lowerGenericInstance(info, inst)); + functions.push( + this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), + ); } catch (e) { if (!(e instanceof PoisonError)) throw e; } @@ -7446,7 +7509,10 @@ export class Lowerer { // instances of its own; the joint fixpoint covers both). while (es < this.emitSpecQueue.length) { try { - const fn = lowerEmitOverrideSpec(this, this.emitSpecQueue[es++]!); + const spec = this.emitSpecQueue[es++]!; + const fn = this.withClosureRoot(emitSpecRoot(spec), () => + lowerEmitOverrideSpec(this, spec), + ); if (fn) functions.push(fn); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -8000,7 +8066,9 @@ export class Lowerer { this.genericClassDemandPriority.set(info, ref); instanceFunctions.push( ...this.withGenericDemandOwner(owner, () => - this.shapes.withDeclaredOrderPriority(ref, () => this.lowerClassMembers(info)), + this.shapes.withDeclaredOrderPriority(ref, () => + this.withClosureRoot(info.def.name, () => this.lowerClassMembers(info)), + ), ), ); relieveHeapPressure(); @@ -8027,7 +8095,7 @@ export class Lowerer { instanceFunctions.push( this.withGenericDemandOwner(owner, () => this.shapes.withDeclaredOrderPriority(ref, () => - this.lowerGenericInstance(info, inst), + this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), ), ), ); @@ -8060,9 +8128,12 @@ export class Lowerer { ); while (specLowered < waveEnd) { try { - const fn = this.shapes.withDeclaredOrderPriority([4, instanceMetadataOrder++], () => - lowerEmitOverrideSpec(this, this.emitSpecQueue[specLowered++]!), - ); + const fn = this.shapes.withDeclaredOrderPriority([4, instanceMetadataOrder++], () => { + const spec = this.emitSpecQueue[specLowered++]!; + return this.withClosureRoot(emitSpecRoot(spec), () => + lowerEmitOverrideSpec(this, spec), + ); + }); if (fn) instanceFunctions.push(fn); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -8090,7 +8161,9 @@ export class Lowerer { initFunctions.push( this.withGenericDemandOwner(owner, () => this.shapes.withDeclaredOrderPriority(priority, () => - this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!), + this.withClosureRoot(this.initNameOf.get(fp.sf)!, () => + this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!), + ), ), ), ); @@ -8116,7 +8189,9 @@ export class Lowerer { const priority = metadataPriority.get(name) ?? [3, expressionMetadataOrder++]; const owner = demandOwner(priority); const fn = this.withGenericDemandOwner(owner, () => - this.shapes.withDeclaredOrderPriority(priority, unit.lower), + this.shapes.withDeclaredOrderPriority(priority, () => + this.withClosureRoot(name, unit.lower), + ), ); if (fn) loweredUnits.set(name, fn); relieveHeapPressure(); @@ -13504,7 +13579,7 @@ export class Lowerer { const key = `${fn.valueFn ?? fn.fn}:${typeKey(funcType)}:${adapterShape}`; let fnName = this.builtinCallableValueFns.get(key); if (!fnName) { - fnName = `%builtin.value.${fn.valueFn ?? fn.fn}.${this.builtinCallableValueFns.size}`; + fnName = keyedHelperName(this, `%builtin.value.${fn.valueFn ?? fn.fn}`, key); this.builtinCallableValueFns.set(key, fnName); this.builtinCallableValueNames.set(fnName, display.slice(display.lastIndexOf(".") + 1)); const params = funcType.params.map((type, index) => ({ diff --git a/packages/compiler/src/frontend/lowering/native-ffi.ts b/packages/compiler/src/frontend/lowering/native-ffi.ts index 0f932ba930..62f2be9f1f 100644 --- a/packages/compiler/src/frontend/lowering/native-ffi.ts +++ b/packages/compiler/src/frontend/lowering/native-ffi.ts @@ -562,7 +562,7 @@ export function lowerFfiMemoryModule(lowerer: Lowerer, loc: SrcLoc): IrExpr { const entries = lowerer.ffiImports .filter((entry) => entry.library !== undefined) .map((entry) => { - const name = `%ffi.module.${lowerer.lambdaCounter++}`; + const name = lowerer.closureName(loc, "", "%ffi.module"); const sourceTypes = ffiSourceParamTypes(entry.params); const locals: IrLocal[] = sourceTypes.map((_, i) => ({ id: `%arg${i}`, diff --git a/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts b/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts index 855da3bda1..388ac3904f 100644 --- a/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts +++ b/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { BOOL, STRING, @@ -27,7 +28,7 @@ export function objectEnumerationReceiver( const key = `obj.receiver:${source.unionId}:${expected.shapeId}`; let name = lowerer.arrHofHelpers.get(key); if (name === undefined) { - name = `%obj.receiver.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%obj.receiver", key); lowerer.arrHofHelpers.set(key, name); const value = varRef("value.0", source, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/pairs-snapshot.ts b/packages/compiler/src/frontend/lowering/pairs-snapshot.ts index 3b33fb0cde..acf7b3bf9b 100644 --- a/packages/compiler/src/frontend/lowering/pairs-snapshot.ts +++ b/packages/compiler/src/frontend/lowering/pairs-snapshot.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { arrayOf, BOOL, @@ -49,7 +50,7 @@ export function pairsSnapshotHelper( const key = `${options.keyPrefix}.snapshot:${shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%${options.keyPrefix}.snapshot.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, `%${options.keyPrefix}.snapshot`, key); lowerer.valueHelpers.set(key, name); const recordType: IrType = { kind: "record", shapeId }; const pairsType = arrayOf(STRING); diff --git a/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts index 09d228b05e..84aa8dac09 100644 --- a/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts +++ b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { varRef } from "../../ir/build.js"; import { type IrExpr, @@ -40,7 +41,7 @@ export function adaptPromiseExecutor( const key = `promise.executor:${typeKey(execT)}:${typeKey(inner)}`; let name = lowerer.arrHofHelpers.get(key); if (name === undefined) { - name = `%promise.executor.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%promise.executor", key); lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; const captured: IrLocal = { id: "f.0", name: "f", type: execT, mutable: false, boxed: true }; diff --git a/packages/compiler/src/frontend/lowering/require-initialization.ts b/packages/compiler/src/frontend/lowering/require-initialization.ts index d979423b0e..c8fc9c7309 100644 --- a/packages/compiler/src/frontend/lowering/require-initialization.ts +++ b/packages/compiler/src/frontend/lowering/require-initialization.ts @@ -9,6 +9,7 @@ * and every reference that can run earlier checks it first — the same * module.tdzCheck the import-cycle bindings use. A reference whose lowering * leaves no expression to check keeps the SC1013 fence. */ +import { positionTag } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { @@ -119,7 +120,7 @@ export function markRequireEarlyBindings(lowerer: Lowerer): void { const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, ""); const bySymbol = new Map(); for (const { decl } of declared) { - const flagId = `%g.${tag}%required.${ts.tokenStartOfNode(decl)}`; + const flagId = `%g.${tag}%required.${positionTag(lowerer, decl)}`; lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true }); const flag: RequireInitFlag = { flagId, names: new Map() }; for (const id of boundNames(decl.name)) { diff --git a/packages/compiler/src/frontend/lowering/stable-names.ts b/packages/compiler/src/frontend/lowering/stable-names.ts new file mode 100644 index 0000000000..8f7d0967e4 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/stable-names.ts @@ -0,0 +1,48 @@ +import { InternalCompilerError } from "../../errors.js"; + +/** The owner of the names keyedHelperName hands out. */ +export interface KeyedNameOwner { + /** Each keyed name with a second hash of the key it was made from, which + * tells a repeated key from a different key with the same name. */ + readonly keyedNameKeys: Map; +} + +/** 48 bits of two FNV-1a hashes of `key`, as 12 hex digits. */ +function keyHash(key: string): string { + let a = 0x811c9dc5; + let b = 0x01000193 ^ 0x5bd1e995; + for (let i = 0; i < key.length; i++) { + const c = key.charCodeAt(i); + a = Math.imul(a ^ c, 0x01000193); + b = Math.imul(b ^ c, 0x01000193) ^ (b >>> 13); + } + const high = (b >>> 0).toString(16).padStart(8, "0").slice(4); + return high + (a >>> 0).toString(16).padStart(8, "0"); +} + +/** A 32-bit hash of `key` independent of keyHash's bits. */ +function checkHash(key: string): number { + let h = 0x9e3779b9; + for (let i = 0; i < key.length; i++) + h = Math.imul(h ^ key.charCodeAt(i), 0x85ebca6b) ^ (h >>> 15); + return h >>> 0; +} + +/** The name of a helper interned by `key`: ``. It depends only on the key, not on how many helpers were interned + * before, so a helper has the same name whichever unit first needs it. Two + * keys that hash to the same name throw an InternalCompilerError. */ +export function keyedHelperName( + owner: KeyedNameOwner, + prefix: string, + key: string, + separator = ".", +): string { + const name = `${prefix}${separator}${keyHash(key)}`; + const check = checkHash(key); + const previous = owner.keyedNameKeys.get(name); + if (previous === undefined) owner.keyedNameKeys.set(name, check); + else if (previous !== check) + throw new InternalCompilerError(`helper name ${name} is shared by two helper keys`); + return name; +} diff --git a/packages/compiler/src/frontend/lowering/strict-equality.ts b/packages/compiler/src/frontend/lowering/strict-equality.ts index 7b9b6902b6..8a87c8111b 100644 --- a/packages/compiler/src/frontend/lowering/strict-equality.ts +++ b/packages/compiler/src/frontend/lowering/strict-equality.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { boolLit, varRef } from "../../ir/build.js"; import { BOOL, @@ -135,7 +136,7 @@ export function lowerUnionEquality( loc, ); if (!result) return null; - name = `%value.eq.${lowerer.equalityHelpers.size}`; + name = keyedHelperName(lowerer, "%value.eq", key); lowerer.equalityHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/structural-positions.ts b/packages/compiler/src/frontend/lowering/structural-positions.ts new file mode 100644 index 0000000000..43b6d4a797 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/structural-positions.ts @@ -0,0 +1,111 @@ +import * as ts from "../ts7/adapter.js"; +import { keyedHelperName, type KeyedNameOwner } from "./stable-names.js"; + +/** The declaration kinds whose names anchor the positions inside them. */ +const ANCHOR_KINDS: ReadonlySet = new Set([ + ts.SyntaxKind.FunctionDeclaration, + ts.SyntaxKind.ClassDeclaration, + ts.SyntaxKind.ClassExpression, + ts.SyntaxKind.VariableDeclaration, + ts.SyntaxKind.MethodDeclaration, + ts.SyntaxKind.GetAccessor, + ts.SyntaxKind.SetAccessor, + ts.SyntaxKind.PropertyDeclaration, + ts.SyntaxKind.PropertyAssignment, + ts.SyntaxKind.ModuleDeclaration, + ts.SyntaxKind.EnumDeclaration, + ts.SyntaxKind.Constructor, + ts.SyntaxKind.InterfaceDeclaration, + ts.SyntaxKind.TypeAliasDeclaration, +]); + +/** The name of `node` when it is a named declaration that anchors the + * positions of the nodes inside it, or null. */ +function anchorName(node: ts.Node): string | null { + return ts.declarationNameAt(node, node.index, ANCHOR_KINDS); +} + +/** The nearest enclosing anchor of `node` (not `node` itself), or its + * source file. */ +function anchorOf(node: ts.Node): ts.Node { + for (let parent = node.parent; parent !== undefined; parent = parent.parent) { + if (ts.isSourceFile(parent) || anchorName(parent) !== null) return parent; + } + return ts.getSourceFileOfNode(node); +} + +/** Source positions that stay the same when code outside the enclosing + * declaration changes. A node's structural position is its file, the chain + * of enclosing named declarations (each with its kind, its name and its + * ordinal among the declarations of the same kind and name in its own + * anchor), and its ordinal among the nodes of its kind whose nearest anchor + * is the same declaration. Editing another declaration, or code nested in another + * named declaration of the same anchor, leaves the position unchanged. */ +export class StructuralPositions { + /** Per anchor and key: the indices of the nodes with that key whose + * nearest anchor it is, in preorder (anchoredNodes). */ + private readonly nodesByKey = new Map>(); + private readonly paths = new Map(); + + /** `fileKey` names a source file in positions (its file name when + * absent). */ + constructor(private readonly fileKey?: (sf: ts.SourceFile) => string) {} + + /** The ordinal of `node` among the nodes of `anchor` with its key. */ + private ordinalIn(anchor: ts.Node, node: ts.Node, key: string): number { + let byKey = this.nodesByKey.get(anchor); + if (byKey === undefined) this.nodesByKey.set(anchor, (byKey = new Map())); + let indices = byKey.get(key); + if (indices === undefined) { + indices = ts.anchoredNodes(anchor, key, ANCHOR_KINDS); + byKey.set(key, indices); + } + // Indices increase in preorder. + let low = 0; + let high = indices.length - 1; + while (low <= high) { + const middle = (low + high) >> 1; + const at = indices[middle]!; + if (at === node.index) return middle; + if (at < node.index) low = middle + 1; + else high = middle - 1; + } + return -1; + } + + /** The anchor chain of `anchor` from the file down. */ + private pathOf(anchor: ts.Node): string { + const cached = this.paths.get(anchor); + if (cached !== undefined) return cached; + let path: string; + if (ts.isSourceFile(anchor)) path = this.fileKey?.(anchor) ?? anchor.fileName; + else { + const parent = anchorOf(anchor); + // The kind keeps declarations of one name apart: a getter and its + // setter, or a class and the namespace merged with it. + const key = `${anchor.kind}:${anchorName(anchor)}`; + path = `${this.pathOf(parent)}/${key}#${this.ordinalIn(parent, anchor, key)}`; + } + this.paths.set(anchor, path); + return path; + } + + /** The structural position of `node`. */ + of(node: ts.Node): string { + const anchor = anchorOf(node); + const name = anchorName(node); + const key = name === null ? `${node.kind}` : `${node.kind}:${name}`; + return `${this.pathOf(anchor)}|${key}#${this.ordinalIn(anchor, node, key)}`; + } +} + +/** The owner of structural positions and the names made from them. */ +export interface PositionTagOwner extends KeyedNameOwner { + readonly positions: StructuralPositions; +} + +/** A short tag for `node`'s structural position, for names that must stay + * unique per source node (class expressions, namespace blocks). */ +export function positionTag(owner: PositionTagOwner, node: ts.Node): string { + return keyedHelperName(owner, "", owner.positions.of(node), ""); +} diff --git a/packages/compiler/src/frontend/ts7/ast.ts b/packages/compiler/src/frontend/ts7/ast.ts index 369213d896..0bddd1a807 100644 --- a/packages/compiler/src/frontend/ts7/ast.ts +++ b/packages/compiler/src/frontend/ts7/ast.ts @@ -47,8 +47,9 @@ import { } from "./ast-guards.generated.js"; import type { Ts7DiagnosticData as Diagnostic } from "./session-schema.generated.js"; import { ModifierFlags, SyntaxKind } from "./enums.js"; -import { AstKind } from "./ast-schema.generated.js"; +import { AstKind, KIND_NODE_LIST } from "./ast-schema.generated.js"; import { PREORDER_LIST } from "./ast-wire.js"; +import { tokenStartOfNode } from "./node-functions.js"; /* ---- everything 7 kept under the same name ---- */ @@ -196,6 +197,93 @@ function walkLinkedPreorder( } } +/** The semantic children of a wire node: list items stand in for their + * list, and attached JSDoc trees are left out (the walkPreorder view). */ +function semanticChildIndices(root: Node, index: number, out: number[]): void { + const wire = root.file.wire; + for (let child = wire.firstChild(index); child !== 0; child = wire.next(child)) { + const kind = wire.kind(child); + if (kind === KIND_NODE_LIST) { + for (let item = wire.firstChild(child); item !== 0; item = wire.next(item)) out.push(item); + } else if (kind !== AstKind.JSDoc) out.push(child); + } +} + +/** The outermost node of `sourceFile` whose token span is exactly + * [start, end), or undefined. Only nodes ending at `end` are + * materialized. */ +export function nodeAtSpan(sourceFile: SourceFile, start: number, end: number): Node | undefined { + const wire = sourceFile.file.wire; + const children: number[] = []; + let index = sourceFile.index; + for (;;) { + children.length = 0; + semanticChildIndices(sourceFile, index, children); + let next = 0; + for (const child of children) { + if (wire.pos(child) > start || wire.end(child) < end) continue; + if (wire.end(child) === end) { + const node = sourceFile.file.node(child); + if (tokenStartOfNode(node) === start) return node; + } + next = child; + break; + } + if (next === 0) return undefined; + index = next; + } +} + +/** The name of the declaration at wire `index` in `root`'s file when its + * kind is in `kinds`: its identifier, private identifier or string literal + * name, "constructor" for a constructor, or null. Reads the wire without + * materializing nodes. */ +export function declarationNameAt( + root: Node, + index: number, + kinds: ReadonlySet, +): string | null { + const wire = root.file.wire; + const kind = wire.kind(index); + if (!kinds.has(kind)) return null; + if (kind === SyntaxKind.Constructor) return "constructor"; + const name = wire.namedChild(index, "name"); + if (name === 0) return null; + const nameKind = wire.kind(name); + if ( + nameKind !== SyntaxKind.Identifier && + nameKind !== SyntaxKind.PrivateIdentifier && + nameKind !== SyntaxKind.StringLiteral + ) + return null; + return wire.text(name) ?? null; +} + +/** The indices, in preorder, of the nodes below `root` that have `key` and + * are not inside a nested named declaration (declarationNameAt with + * `kinds`). A named declaration is keyed `:` and its subtree is + * not visited; other nodes are keyed by their kind. No node is + * materialized. */ +export function anchoredNodes(root: Node, key: string, kinds: ReadonlySet): number[] { + const wire = root.file.wire; + const out: number[] = []; + const children: number[] = []; + const stack: number[] = []; + semanticChildIndices(root, root.index, children); + for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]!); + while (stack.length > 0) { + const index = stack.pop()!; + const kind = wire.kind(index); + const name = declarationNameAt(root, index, kinds); + if ((name === null ? String(kind) : `${kind}:${name}`) === key) out.push(index); + if (name !== null) continue; + children.length = 0; + semanticChildIndices(root, index, children); + for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]!); + } + return out; +} + export function getLineAndCharacterOfPosition( sourceFile: SourceFile, position: number, diff --git a/packages/compiler/src/frontend/type-mapper.test.ts b/packages/compiler/src/frontend/type-mapper.test.ts index cf78554b42..163ba7b9ee 100644 --- a/packages/compiler/src/frontend/type-mapper.test.ts +++ b/packages/compiler/src/frontend/type-mapper.test.ts @@ -41,7 +41,7 @@ describe("union arm lookup", () => { test("sees finalized recursive arms after a missing lookup", () => { const unions = new UnionRegistry(); const recursive = {} as Type; - const union = unions.recursiveRef(recursive); + const union = unions.recursiveRef(recursive, () => "Recursive"); expect(unions.armTag(union, STRING)).toBe(-1); unions.finalizeRecursive(recursive, [ F64, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index af88a74483..9abd5a0b0c 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1,4 +1,5 @@ import { InternalCompilerError } from "../errors.js"; +import { keyedHelperName, type KeyedNameOwner } from "./lowering/stable-names.js"; import { mockCallRecordType, mockContextType } from "./mock-shapes.js"; import * as ts from "./ts7/adapter.js"; import { bodyReadsArguments } from "./arguments-usage.js"; @@ -262,6 +263,8 @@ function comparePriority(left: DeclaredOrderPriority, right: DeclaredOrderPriori export class ShapeRegistry { /** Existing layouts change only when a recursive placeholder closes. */ revision = 0; + /** Each shape id with a second hash of its key (keyedHelperName). */ + readonly keyedNameKeys = new Map(); private readonly byKey = new Map(); private readonly byId = new Map(); /** Historical emit-order rank of the declaration-order metadata each @@ -280,7 +283,7 @@ export class ShapeRegistry { * writer after all priority candidates have closed. */ private readonly derivedDeclaredOrderFinalizers: (() => void)[] = []; private currentDeclaredOrderPriority: DeclaredOrderPriority = [0, 0]; - /** All interned shapes in first-seen (`r0`, `r1`, ...) order. */ + /** All interned shapes in first-seen order. */ readonly shapes: IrRecordShape[] = []; /** ts.Types currently being mapped — a BACK-REFERENCE to one of these is * the recursive knot (`interface TreeNode { children: TreeNode[] }`): @@ -400,11 +403,12 @@ export class ShapeRegistry { /** The shape id a back-reference to an in-progress type resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER - * entry (empty fields) the outer frame finalizes. */ - recursiveRef(t: ts.Type): string { + * entry (empty fields) the outer frame finalizes. `describe` names a new + * placeholder (checkerTypeDescription). */ + recursiveRef(t: ts.Type, describe: () => string): string { let id = this.recIds.get(t); if (id === undefined) { - id = `r${this.shapes.length}`; + id = recursivePlaceholderName(this, "r", describe(), (name) => this.byId.has(name)); const shape: IrRecordShape = { id, fields: [] }; this.byId.set(id, shape); this.shapes.push(shape); @@ -473,7 +477,7 @@ export class ShapeRegistry { const key = this.keyOf(fields, tuple, indexValue); let id = this.byKey.get(key); if (id === undefined) { - id = `r${this.shapes.length}`; + id = keyedHelperName(this, "r", key, ""); const shape: IrRecordShape = { id, fields, @@ -551,6 +555,58 @@ export class ShapeRegistry { } } +/** The id of a recursive placeholder described by `description`. Two + * different types can still share a description (checkerTypeDescription stops + * after a few levels of type arguments); the later one takes the first free + * name with a numbered suffix. */ +function recursivePlaceholderName( + owner: KeyedNameOwner, + prefix: string, + description: string, + taken: (name: string) => boolean, +): string { + let name = keyedHelperName(owner, prefix, `recursive:${description}`, ""); + for (let n = 1; taken(name); n++) + name = keyedHelperName(owner, prefix, `recursive:${description}#${n}`, ""); + return name; +} + +/** How many levels of type arguments checkerTypeDescription describes. */ +const RECURSIVE_NAME_DEPTH = 3; + +/** A checker type's description for names that must not depend on checker + * type ids (a recursive placeholder, a generic class instance keyed by a + * literal): its printed form, the structural position of its first + * declaration, and the same for its type arguments (two instantiations + * print the same when their type arguments are type parameters or classes + * of the same name from different declarations). */ +export function checkerTypeDescription( + t: ts.Type, + ctx: Pick, + depth = 0, +): string { + const symbol = t.getAliasSymbol() ?? t.getSymbol(); + const declaration = symbol?.declarations[0]; + const where = declaration + ? (ctx.positionOf?.(declaration) ?? + `${ts.getSourceFileOfNode(declaration).fileName}:${declaration.pos}`) + : ""; + let description = `${ctx.checker.typeToString(t)}@${where}`; + if (depth < RECURSIVE_NAME_DEPTH) { + const aliasArguments = + t.getAliasSymbol() !== undefined ? (t.getAliasTypeArguments() ?? []) : []; + const typeArguments = + aliasArguments.length > 0 + ? aliasArguments + : t.isTypeReference() + ? ctx.checker.getTypeArguments(t) + : []; + if (typeArguments.length > 0) + description += `<${typeArguments.map((arg) => checkerTypeDescription(arg, ctx, depth + 1)).join(",")}>`; + } + return description; +} + /** The frontend's union interner — mirrors ShapeRegistry. A union's * canonical identity includes its typeKey-sorted arms and literal * discriminator contract. An arm's index in that list IS its runtime tag. @@ -558,10 +614,12 @@ export class ShapeRegistry { export class UnionRegistry { /** Existing arm/discriminator contracts change when a placeholder closes. */ revision = 0; + /** Each union id with a second hash of its key (keyedHelperName). */ + readonly keyedNameKeys = new Map(); private readonly byKey = new Map(); private readonly byId = new Map(); private readonly armTags = new Map>(); - /** All interned unions in first-seen (`u0`, `u1`, ...) order. */ + /** All interned unions in first-seen order. */ readonly unions: IrUnionDef[] = []; /** ts.Types currently being mapped — a back-reference to one is the * recursive knot passing through a union (`type Tree = Leaf | Branch` @@ -577,11 +635,12 @@ export class UnionRegistry { /** The union id a back-reference to an in-progress union resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER - * entry (empty arms) the outer frame finalizes. */ - recursiveRef(t: ts.Type): string { + * entry (empty arms) the outer frame finalizes. `describe` names a new + * placeholder (checkerTypeDescription). */ + recursiveRef(t: ts.Type, describe: () => string): string { let id = this.recIds.get(t); if (id === undefined) { - id = `u${this.unions.length}`; + id = recursivePlaceholderName(this, "u", describe(), (name) => this.byId.has(name)); const def: IrUnionDef = { id, arms: [] }; this.byId.set(id, def); this.unions.push(def); @@ -631,7 +690,7 @@ export class UnionRegistry { const key = JSON.stringify([arms.map(typeKey), discriminant]); let id = this.byKey.get(key); if (id === undefined) { - id = `u${this.unions.length}`; + id = keyedHelperName(this, "u", key, ""); const def: IrUnionDef = { id, arms, ...(discriminant ? { discriminant } : {}) }; this.byKey.set(key, id); this.byId.set(id, def); @@ -1131,6 +1190,10 @@ export interface TypeMapperCtx { * instance being lowered, the instantiated result at its calls. */ localClassValue?: (type: ts.Type) => IrType | null; checker: ts.TypeChecker; + /** The structural position of a declaration (structural-positions.ts), + * which names recursive placeholders independently of where unrelated + * code sits. The Lowerer supplies it. */ + positionOf?: (node: ts.Node) => string; /** JavaScript declarations whose instance or object-literal types only * the checked-dynamic representation holds (js-dynamic-objects.ts). * Absent when the program has none: every mapping is unchanged. */ @@ -4672,7 +4735,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const knownRecursive = unions.recursiveUnionFor(widened); if (knownRecursive !== undefined) return { kind: "union", unionId: knownRecursive }; if (unions.inProgress.has(widened)) { - return { kind: "union", unionId: unions.recursiveRef(widened) }; + return { + kind: "union", + unionId: unions.recursiveRef(widened, () => checkerTypeDescription(widened, ctx)), + }; } unions.inProgress.add(widened); const sensitivityAtEntry = contextResolutions; @@ -7016,7 +7082,10 @@ function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { // frame completes (finalizeRecursive below). Backends represent records // as heap references, so the self-reference is an ordinary pointer. if (shapes.inProgress.has(widened)) { - return { kind: "record", shapeId: shapes.recursiveRef(widened) }; + return { + kind: "record", + shapeId: shapes.recursiveRef(widened, () => checkerTypeDescription(widened, ctx)), + }; } shapes.inProgress.add(widened); const sensitivityAtEntry = contextResolutions; diff --git a/packages/compiler/test/array-pipeline-fusion.test.ts b/packages/compiler/test/array-pipeline-fusion.test.ts index f5d743265f..55cd0025ec 100644 --- a/packages/compiler/test/array-pipeline-fusion.test.ts +++ b/packages/compiler/test/array-pipeline-fusion.test.ts @@ -26,7 +26,7 @@ function helperCalls(fn: IrFunction): string[] { everyStmtList(fn.body, { expr: (e) => { if (e.kind === "call" && e.callee.startsWith("%arr.")) - calls.push(e.callee.replace(/\.\d+$/, "")); + calls.push(e.callee.replace(/\.[0-9a-f]+$/, "")); return true; }, stmt: () => true, diff --git a/packages/compiler/test/format-float-arguments.test.ts b/packages/compiler/test/format-float-arguments.test.ts index e59e023ca7..6ce9c4bc04 100644 --- a/packages/compiler/test/format-float-arguments.test.ts +++ b/packages/compiler/test/format-float-arguments.test.ts @@ -27,7 +27,7 @@ test("util.format %f lowers a boolean argument once", async () => { const helpers = mod.functions .map((f) => f.name) .filter((name) => name.startsWith("%arr.")) - .map((name) => name.replace(/\.\d+$/, "")) + .map((name) => name.replace(/\.[0-9a-f]+$/, "")) .sort(); expect(helpers).toEqual(["%arr.every", "%arr.some"]); } finally { diff --git a/packages/compiler/test/in-runtime-key-emission.test.ts b/packages/compiler/test/in-runtime-key-emission.test.ts index aebbb2ea4f..589685a7a8 100644 --- a/packages/compiler/test/in-runtime-key-emission.test.ts +++ b/packages/compiler/test/in-runtime-key-emission.test.ts @@ -55,7 +55,7 @@ const d: Record = {}; d["a"] = 1; console.log(has(d, "a"), has(d, "b")); `); - const helper = body(llvm, /sc_b?f__x25_rec_haskey_\d+/); + const helper = body(llvm, /sc_b?f__x25_rec_haskey_[0-9a-f]+/); expect(helper).toContain("@scr_map_has_str("); // Not a scan over a fresh key snapshot per probe. expect(helper).not.toContain("@scr_map_keys_js_order("); diff --git a/packages/compiler/test/stable-names.test.ts b/packages/compiler/test/stable-names.test.ts new file mode 100644 index 0000000000..720472556d --- /dev/null +++ b/packages/compiler/test/stable-names.test.ts @@ -0,0 +1,112 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule, type IrModule } from "../src/index.js"; + +/** Lowers each source in turn as the same file, so names that include the + * file's path compare equal. */ +async function lower(...sources: string[]): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-stable-names-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + const modules: IrModule[] = []; + for (const source of sources) { + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + modules.push(deserializeModule(await readFile(outPath, "utf8"))); + } + return modules; + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function names(mod: IrModule): string[] { + return [ + ...mod.records.map((record) => record.id), + ...mod.unions.map((union) => union.id), + ...mod.functions.map((fn) => fn.name), + ...mod.classes.map((cls) => cls.name), + ].sort(); +} + +const recursive = ` +type Tree = { value: number; children: Tree[]; parent: Tree | null }; +function total(tree: Tree): number { + let sum = tree.value; + for (const child of tree.children) sum += total(child); + return sum; +} +const root: Tree = { value: 1, children: [], parent: null }; +root.children.push({ value: 2, children: [], parent: root }); +console.log(total(root)); +`; + +test("recursive record and union ids do not depend on the code above the type", async () => { + const [before, after] = await lower( + recursive, + `// A comment above the type.\n\ntype Unrelated = { name: string };\n\n${recursive}`, + ); + expect(before.records.some((record) => record.fields.some((f) => f.name === "children"))).toBe( + true, + ); + expect(names(after)).toEqual(names(before)); +}); + +test("recursive instantiations that print alike get distinct placeholders", async () => { + const [mod] = await lower(` +namespace A { + export class Item { + a = 1; + } +} +namespace B { + export class Item { + b = "b"; + } +} +type Chain = { item: T; next: Chain | null }; +const first: Chain = { item: new A.Item(), next: null }; +const second: Chain = { item: new B.Item(), next: null }; +first.next = { item: new A.Item(), next: null }; +second.next = { item: new B.Item(), next: null }; +console.log(first.next.item.a, second.next.item.b); +`); + const chains = mod.records.filter((record) => record.fields.some((f) => f.name === "next")); + expect(new Set(chains.map((record) => record.id)).size).toBe(chains.length); + expect(chains.length).toBeGreaterThanOrEqual(2); +}); + +test("a getter and a setter of one name keep their local classes apart", async () => { + const [mod] = await lower(` +class Box { + private v = 0; + get value(): number { + class Local { + n(): number { + return 1; + } + } + return new Local().n() + this.v; + } + set value(x: number) { + class Local { + n(): number { + return 2; + } + } + this.v = x * new Local().n(); + } +} +const box = new Box(); +box.value = 5; +console.log(box.value); +`); + const locals = mod.classes.filter((cls) => cls.name.includes("Local")); + expect(locals).toHaveLength(2); + expect(locals[0]!.name).not.toBe(locals[1]!.name); +}); diff --git a/packages/compiler/test/type-mapper-unions.test.ts b/packages/compiler/test/type-mapper-unions.test.ts index c714df4d75..6849600028 100644 --- a/packages/compiler/test/type-mapper-unions.test.ts +++ b/packages/compiler/test/type-mapper-unions.test.ts @@ -139,7 +139,8 @@ describe("checker and synthesized optional union identity", () => { nullable.arms.filter((arm) => arm.kind !== "nullT" && arm.kind !== "undefinedT"), ), ).toBe(required.unionId); - expect(nullable.discriminant!.cases.map((entry) => entry.values)).toEqual([ + // Cases follow the arms, which are ordered by their type keys. + expect(nullable.discriminant!.cases.map((entry) => entry.values).sort()).toEqual([ ["a", "alias"], ["b"], ]); From ebbc6c1831827649525ff5b3aff5b875a6062a80 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:56:15 -0500 Subject: [PATCH 094/395] Fix recursive JSON unions and primitive constructors as array callbacks --- .../lowering/containers/callback-arguments.ts | 59 +++++++++++++ .../lowering/expressions/array-literals.ts | 15 ++++ .../lowering/expressions/identifiers.ts | 51 ++++++++++-- .../src/frontend/lowering/lower-calls.ts | 34 ++++++++ .../src/frontend/lowering/lower-exprs.ts | 8 +- .../6826-map-primitive-constructors.ts.json | 6 ++ ...27-recursive-union-nested-literals.ts.json | 6 ++ ...828-recursive-union-object-walkers.ts.json | 6 ++ .../corpus/6826-map-primitive-constructors.ts | 25 ++++++ .../6827-recursive-union-nested-literals.ts | 29 +++++++ .../6828-recursive-union-object-walkers.ts | 83 +++++++++++++++++++ .../6826-map-primitive-constructors.ts.json | 1 + ...27-recursive-union-nested-literals.ts.json | 1 + ...828-recursive-union-object-walkers.ts.json | 1 + 14 files changed, 316 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6826-map-primitive-constructors.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6827-recursive-union-nested-literals.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6828-recursive-union-object-walkers.ts.json create mode 100644 tests/corpus/6826-map-primitive-constructors.ts create mode 100644 tests/corpus/6827-recursive-union-nested-literals.ts create mode 100644 tests/corpus/6828-recursive-union-object-walkers.ts create mode 100644 tests/harness/corpus-costs/6826-map-primitive-constructors.ts.json create mode 100644 tests/harness/corpus-costs/6827-recursive-union-nested-literals.ts.json create mode 100644 tests/harness/corpus-costs/6828-recursive-union-object-walkers.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 f39c8e86c7..9db04e95b5 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -6,7 +6,9 @@ import { varRef } from "../../../ir/build.js"; import { typeKey } from "../../type-mapper.js"; import { BOOL, + DYN, F64, + STRING, type IrExpr, type IrType, type SrcLoc, @@ -69,6 +71,20 @@ export function lowerArrayCallback( if (valueT.kind === "union") lowerer.requireTruthyUnion(valueT.unionId, argNode); return { fnArg: truthyFilterCallback(lowerer, valueT, locOf(argNode)), arity: 1 }; } + // The ambient `String` (`values.map(String)`): String(value) converts its + // first argument as a direct call does, whatever the element type; the + // constructor's own value form takes a string. + if ( + ts.isIdentifier(argNode) && + argNode.text === "String" && + !isJsSourceFile(ts.getSourceFileOfNode(argNode)) && + lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode) ?? undefined) + ) { + const valueT = lead[0]; + if (valueT === undefined || valueT.kind === "void") + lowerer.badType(argNode, lowerer.typeOf(argNode)); + return { fnArg: stringConversionCallback(lowerer, valueT, argNode), arity: 1 }; + } const full = [...lead, F64, arrT]; // A DYN-receiver HOF's callback (`parsed.flatMap((value) => ...)`): // the contextual signature types the unannotated param `any` (the @@ -274,6 +290,49 @@ function runtimeOptionalPairVariant(lowerer: Lowerer, expected: IrType, mapped: return lowerer.isRuntimeOptionalRecordVariant(strip(expected), strip(mapped)); } +/** `String` as a one-parameter callback over `valueT`: the conversion a + * direct `String(value)` call lowers to (the runtime's String() for unknown + * values and unions holding a symbol, ToString otherwise), interned per + * value type. */ +function stringConversionCallback( + lowerer: Lowerer, + valueT: IrType, + node: ts.Expression, +): IrExpr & { type: IrType & { kind: "func" } } { + const loc = locOf(node); + const fnT: IrType & { kind: "func" } = { kind: "func", params: [valueT], ret: STRING }; + const key = `mapString:${typeKey(valueT)}`; + let name = lowerer.arrHofHelpers.get(key); + if (!name) { + name = `%arr.mapString.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, name); + const value = varRef("v.0", valueT, loc); + const runtimeString = + valueT.kind === "dyn" || + (valueT.kind === "union" && + lowerer.unions.get(valueT.unionId)?.arms.some((arm) => arm.kind === "symbol") === true && + lowerer.dynConvertible(valueT)); + const converted: IrExpr = runtimeString + ? { + kind: "libCall", + fn: "dyn.stringConstructor", + args: [lowerer.coerceToExpected(value, DYN)], + type: STRING, + loc, + } + : lowerer.ensureString(value, node); + lowerer.liftedFns.push({ + name, + params: [{ localId: "v.0", name: "v", type: valueT }], + returnType: STRING, + locals: [{ id: "v.0", name: "v", type: valueT, mutable: false }], + body: [{ kind: "return", value: converted, loc }], + loc, + }); + } + return { kind: "closure", fnName: name, captures: [], type: fnT, loc }; +} + /** A native closure for the ambient Boolean callback. Reifying the * JavaScript constructor itself is unnecessary; Array#filter only observes * its one-argument ToBoolean result. */ diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 00eae1041c..8f119504cb 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -184,6 +184,21 @@ export function lowerArrayLiteral( if (arms.length === 1) mapped = arms[0]!; } } + // A NON-EMPTY literal whose own type is an array, under a union with + // exactly one array arm and no tuple arm (`[1, { n: true }]` in a + // `Json[]` arm of a recursive `Json`): the checker types each element + // from the arm, so its object literals lower as the arm's elements, + // and the literal's own element type (`number | { n: boolean }`) + // cannot hold them. Build it as the arm; its value only flows into the + // union slot. + if (ctxUnion && expr.elements.nodes.length > 0 && mapped?.kind === "array") { + const def = lowerer.unions.get(ctxUnion.unionId); + const arrayArms = def?.arms.filter((a) => a.kind === "array") ?? []; + const tupleArms = + def?.arms.filter((a) => a.kind === "record" && !!lowerer.shapes.get(a.shapeId)?.tuple) ?? + []; + if (arrayArms.length === 1 && tupleArms.length === 0) mapped = arrayArms[0]!; + } } // A Date's toJSON() element keeps its null in a literal with no declared // destination: the lib types say string. diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 16cca3b13a..1f74cea429 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -11,6 +11,7 @@ import { DYN, F64, type IrExpr, + type IrLocal, type IrType, STRING, type SrcLoc, @@ -226,7 +227,10 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: if (use?.kind === "element") { const key = runtimeOptionalElementKey(use.access.argumentExpression); if (key === null) { - return varRef(local.id, local.type, loc); + return ( + runtimeOptionalKeyedRecord(lowerer, expr, local, loc) ?? + varRef(local.id, local.type, loc) + ); } return runtimeOptionalReceiverRead(lowerer, expr, local, key, loc); } @@ -938,10 +942,43 @@ function moduleFileName(lowerer: Lowerer, sf: ts.SourceFile): string { : sf.fileName; } -/** Object.keys/Object.entries require an object receiver, but their first - * argument is not syntactically a member receiver. Recognize that one - * standard-library position so a runtime-optional loop element can be - * checked against the checker-narrowed record arm before builtin lowering. */ +/** A computed-key access (`value[key]` after `typeof value === "object"`) + * of a runtime-optional binding the checker narrowed to one record arm of a + * wider union. The binding re-tags to that record or undefined, so the keyed + * read or write evaluates its operands and then throws Node's TypeError for + * an undefined receiver. The other arms, which the checker excluded, fail + * the checked re-tag. Null when the binding stores only the record and + * undefined, or the narrowed record is not one of its arms. */ +function runtimeOptionalKeyedRecord( + lowerer: Lowerer, + expr: ts.Identifier, + local: IrLocal, + loc: SrcLoc, +): IrExpr | null { + if (local.type.kind !== "union") return null; + const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (narrowed?.kind !== "record") return null; + const fromId = local.type.unionId; + const arms = lowerer.unions.get(fromId)?.arms; + const recordTag = lowerer.armTag(fromId, narrowed); + const undefTag = lowerer.armTag(fromId, UNDEFINED_T); + if (!arms || arms.length <= 2 || recordTag < 0 || undefTag < 0) return null; + const target = lowerer.withUndefinedArm(narrowed); + if (target.kind !== "union") return null; + const trappable = new Set(); + for (let tag = 0; tag < arms.length; tag++) { + if (tag !== recordTag && tag !== undefTag) trappable.add(tag); + } + const helper = lowerer.unionRetagHelper(fromId, target.unionId, loc, trappable); + if (!helper) return null; + return lowerer.checkedNarrowCall(helper, varRef(local.id, local.type, loc), target, loc); +} + +/** Object.keys, Object.values and Object.entries require an object + * receiver, but their first argument is not syntactically a member + * receiver. Recognize that one standard-library position so a + * runtime-optional loop element can be checked against the checker-narrowed + * record arm before builtin lowering. */ function runtimeOptionalObjectWalkerArg(lowerer: Lowerer, expr: ts.Expression): boolean { let arg = expr; while ( @@ -962,7 +999,9 @@ function runtimeOptionalObjectWalkerArg(lowerer: Lowerer, expr: ts.Expression): return false; const access = call.expression; return ( - (access.name.text === "keys" || access.name.text === "entries") && + (access.name.text === "keys" || + access.name.text === "values" || + access.name.text === "entries") && lowerer.isStdlibMember(access) ); } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index c5e34306cd..46f98871e4 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -173,6 +173,7 @@ import { stringWrapperToString, symbolFieldInfo, templateRawTextOf, + primitiveCtorClosure, } from "./lower-exprs.js"; import { isSafeToDiscard } from "./expressions/evaluation-safety.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; @@ -4343,7 +4344,25 @@ export function lowerDynDispatchMethodCall( } } } + // `String`, `Number` or `Boolean` as the callback of an array method + // (`values.map(String)` over an unknown array): the method calls it with + // each element, whatever its type, and cannot observe which function + // value it received, so it gets the constructor's form that converts any + // argument rather than the string-input form typed code stores. + const arrayCallbackReceiver = + ARRAY_CALLBACK_METHODS.has(method) && + lowerer.checker.isArrayType(lowerer.typeOf(access.expression)); const args = call.arguments.nodes.map((arg, i) => { + const primitive = + i === 0 && arrayCallbackReceiver && ts.isIdentifier(arg) + ? stdlibGlobalNameOf(lowerer, arg) + : null; + if (primitive === "String" || primitive === "Number" || primitive === "Boolean") + return lowerer.coerceInto( + arg, + primitiveCtorClosure(lowerer, primitive, locOf(arg), true), + DYN, + ); if (i === 0 && predicate) return lowerer.coerceInto(arg, predicate, DYN); const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, arg); return undefinedArg @@ -4361,6 +4380,21 @@ export function lowerDynDispatchMethodCall( }; } +/** Array.prototype methods whose first argument is a callback they call + * with the elements. */ +const ARRAY_CALLBACK_METHODS = new Set([ + "map", + "filter", + "forEach", + "some", + "every", + "find", + "findIndex", + "findLast", + "findLastIndex", + "flatMap", +]); + /** Every String.prototype method name a typed or dyn string lowering * answers. */ function isStringMethodName(name: string): boolean { diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index ba96bea8ca..b5acaf9e62 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -3057,8 +3057,10 @@ export function lowerFieldCompound( * returns the zero-capture closure over it. Typed sources use the mapped * `(value: string) => primitive` signature; a recognized JavaScript * concise-arrow selector (`() => String`, the picocolors fallback) takes - * dyn so its ordinary coercion signature survives inference residue. The - * dyn bodies run exact ToString/ToNumber/ToBoolean, including object hooks. + * dyn so its ordinary coercion signature survives inference residue, and a + * boxed constructor value takes dyn too (coerceToExpected). The dyn bodies + * run what the constructors do for any argument: String() (a symbol's + * description form included), ToNumber and ToBoolean, with object hooks. * Interning makes every reference under one ABI the SAME immortal closure, * so `opt.type === String` compares like JS function identity. */ export function primitiveCtorClosure( @@ -3081,7 +3083,7 @@ export function primitiveCtorClosure( ? name === "BigInt" ? { kind: "libCall", fn: "dyn.bigintConstructor", args: [v], type: BIGINT_T, loc } : name === "String" - ? { kind: "libCall", fn: "dyn.toStringCoerce", args: [v], type: STRING, loc } + ? { kind: "libCall", fn: "dyn.stringConstructor", args: [v], type: STRING, loc } : name === "Number" ? { kind: "libCall", fn: "dyn.numberConstructor", args: [v], type: F64, loc } : { kind: "dynTest", test: "truthy", value: v, type: BOOL, loc } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6826-map-primitive-constructors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6826-map-primitive-constructors.ts.json new file mode 100644 index 0000000000..0bf7b0871a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6826-map-primitive-constructors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6826-map-primitive-constructors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6827-recursive-union-nested-literals.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6827-recursive-union-nested-literals.ts.json new file mode 100644 index 0000000000..6e11aa6858 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6827-recursive-union-nested-literals.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6827-recursive-union-nested-literals.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6828-recursive-union-object-walkers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6828-recursive-union-object-walkers.ts.json new file mode 100644 index 0000000000..65c965b9e5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6828-recursive-union-object-walkers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6828-recursive-union-object-walkers.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6826-map-primitive-constructors.ts b/tests/corpus/6826-map-primitive-constructors.ts new file mode 100644 index 0000000000..f6d61bd485 --- /dev/null +++ b/tests/corpus/6826-map-primitive-constructors.ts @@ -0,0 +1,25 @@ +// The String, Number and Boolean constructors passed as callbacks: over +// unknown arrays they convert any element, over typed arrays String converts +// each element as String(value) does, and the boxed values keep identity. +const values: unknown[] = [1, "b", true, null, undefined, Symbol("s"), [1, 2], { a: 1 }, 2n]; +console.log(values.map(String).join("|")); +console.log(values.slice(0, 5).map(Number), values.map(Boolean)); + +function joinAll(...parts: unknown[]): string { + return parts.map(String).join(","); +} +console.log(joinAll(1, "a", false, null)); + +const numbers = [1, 2.5, -0, NaN, Infinity]; +console.log(numbers.map(String)); +const mixed: (number | string | boolean)[] = [1, "x", false]; +console.log(mixed.map(String)); +const symbols: (symbol | string)[] = [Symbol("q"), "r", Symbol()]; +console.log(symbols.map(String)); +console.log([10n, -3n].map(String)); +console.log(["1", " 2 ", "x", ""].map(Number), ["", "0", "a"].map(Boolean)); + +const boxed: unknown = String; +const table: { [name: string]: unknown } = { s: String, n: Number, b: Boolean }; +console.log(boxed === String, table.n === Number, table.b === String, typeof boxed); +console.log(values.filter(Boolean).length, values.some(Boolean), numbers.every(Boolean)); diff --git a/tests/corpus/6827-recursive-union-nested-literals.ts b/tests/corpus/6827-recursive-union-nested-literals.ts new file mode 100644 index 0000000000..900bbf9775 --- /dev/null +++ b/tests/corpus/6827-recursive-union-nested-literals.ts @@ -0,0 +1,29 @@ +// A recursive JSON-like union with an array arm and an index-signature +// record arm: nested literals mix records into arrays and arrays into +// records at every depth. +type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +function show(value: Json): string { + return JSON.stringify(value); +} + +const config: Json = { list: [1, "two", null, { nested: [true, { deeper: [] }] }], flag: false }; +console.log(show(config)); +console.log(show({ list: [{ n: true }] })); +console.log(show([1, { k: null }, [{ a: [2] }]])); +console.log(show({ a: { b: [{ c: "x" }, 3] } })); + +const rows: Json[] = [{ id: 1, tags: ["a", { t: 2 }] }, [null, { x: [] }]]; +console.log(rows.map(show).join(" | ")); + +let value: Json = []; +value = { replaced: [1, { again: [true] }] }; +console.log(show(value)); + +const doc: { [key: string]: Json } = { items: [{ name: "x", parts: [1, { p: "q" }] }] }; +console.log(show(doc)); +const items = doc["items"]; +if (Array.isArray(items)) { + items.push({ added: [9] }); + console.log(items.length, show(items)); +} diff --git a/tests/corpus/6828-recursive-union-object-walkers.ts b/tests/corpus/6828-recursive-union-object-walkers.ts new file mode 100644 index 0000000000..5c854eb94c --- /dev/null +++ b/tests/corpus/6828-recursive-union-object-walkers.ts @@ -0,0 +1,83 @@ +// Recursive walkers over a JSON-like union: the array branch calls the +// walker for each element, and the record branch reads the narrowed record +// with Object.values, Object.keys and Object.entries. +type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +function depth(value: Json): number { + if (Array.isArray(value)) return 1 + Math.max(0, ...value.map(depth)); + if (value !== null && typeof value === "object") return 1 + Math.max(0, ...Object.values(value).map(depth)); + return 0; +} + +function count(value: Json): number { + let total = 1; + if (Array.isArray(value)) { + for (const item of value) total += count(item); + } else if (value !== null && typeof value === "object") { + for (const item of Object.values(value)) total += count(item); + } + return total; +} + +function paths(value: Json, prefix: string): string[] { + if (Array.isArray(value)) return value.flatMap((item, index) => paths(item, `${prefix}[${index}]`)); + if (value !== null && typeof value === "object") { + const out: string[] = []; + for (const key of Object.keys(value)) out.push(...paths(value[key]!, `${prefix}.${key}`)); + for (const [key, item] of Object.entries(value)) if (item === null) out.push(`${prefix}.${key}=null`); + return out; + } + return [`${prefix}=${String(value)}`]; +} + +const doc: Json = JSON.parse('{"a":[1,{"b":[true,null]}],"c":{"d":"x"},"e":[]}'); +console.log(depth(doc), count(doc)); +console.log(paths(doc, "$").join(" ")); +console.log(depth(3), count([[[]]]), paths([], "$")); + +// A computed-key read or write in a branch that an array hole reaches throws +// Node's TypeError after the key (and, for a write, the value) evaluates. +type Bag = number | string | number[] | { [key: string]: number }; +const keys: string[] = []; +function keyOf(k: string): string { + keys.push(k); + return k; +} +function isBag(value: Bag): value is { [key: string]: number } { + return !Array.isArray(value) && typeof value !== "string" && typeof value !== "number"; +} +function bump(value: Bag, k: string): string { + if (isBag(value)) { + value[keyOf(k)] = (value[keyOf(k)] ?? 0) + 1; + return String(value[keyOf(k)]); + } + return "-"; +} +function put(value: Bag, k: string): string { + if (isBag(value)) { + value[keyOf(k)] = keys.length; + return "put"; + } + return "-"; +} +function peek(value: Bag, k: string): string { + if (isBag(value)) return typeof value[keyOf(k)]; + return "-"; +} +const bags: Bag[] = [{ a: 1 }, "s", [1]]; +bags.length = 4; +for (const which of ["bump", "put", "peek"]) { + const out: string[] = []; + for (let i = 0; i < bags.length; i++) { + try { + const k = "a" + which.length; + if (which === "bump") out.push(bump(bags[i], k)); + else if (which === "put") out.push(put(bags[i], k)); + else out.push(peek(bags[i], k)); + } catch (e) { + out.push(e instanceof TypeError ? e.message : "other"); + } + } + console.log(out.join(" | ")); +} +console.log(keys.join(","), JSON.stringify(bags)); diff --git a/tests/harness/corpus-costs/6826-map-primitive-constructors.ts.json b/tests/harness/corpus-costs/6826-map-primitive-constructors.ts.json new file mode 100644 index 0000000000..d2c7958296 --- /dev/null +++ b/tests/harness/corpus-costs/6826-map-primitive-constructors.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 3500 } diff --git a/tests/harness/corpus-costs/6827-recursive-union-nested-literals.ts.json b/tests/harness/corpus-costs/6827-recursive-union-nested-literals.ts.json new file mode 100644 index 0000000000..f44e89d4f4 --- /dev/null +++ b/tests/harness/corpus-costs/6827-recursive-union-nested-literals.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3400 } diff --git a/tests/harness/corpus-costs/6828-recursive-union-object-walkers.ts.json b/tests/harness/corpus-costs/6828-recursive-union-object-walkers.ts.json new file mode 100644 index 0000000000..77ddc81f82 --- /dev/null +++ b/tests/harness/corpus-costs/6828-recursive-union-object-walkers.ts.json @@ -0,0 +1 @@ +{ "plain": 4200, "san": 5800 } From b99094543f67f874867a3eece221c8b2fde463e7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 04:04:16 -0500 Subject: [PATCH 095/395] Revert "Read error constructors as values and destructure workerData in JavaScript" This reverts #1776, which made main CI fail. --- .../lowering/expressions/object-literals.ts | 25 +----- .../lowering/expressions/property-access.ts | 20 +---- .../src/frontend/lowering/lower-stmts.ts | 3 - .../6870-error-constructor-values.js.json | 6 -- .../6871-worker-data-destructuring.cjs.json | 6 -- packages/runtime/src/scr_dyn.h | 3 - packages/runtime/src/scr_dyn_invoke.c | 2 - packages/runtime/src/scr_json.c | 79 +++---------------- tests/corpus/6870-error-constructor-values.js | 55 ------------- .../corpus/6871-worker-data-destructuring.cjs | 30 ------- .../6870-error-constructor-values.js.json | 1 - .../6871-worker-data-destructuring.cjs.json | 1 - 12 files changed, 15 insertions(+), 216 deletions(-) delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json delete mode 100644 tests/corpus/6870-error-constructor-values.js delete mode 100644 tests/corpus/6871-worker-data-destructuring.cjs delete mode 100644 tests/harness/corpus-costs/6870-error-constructor-values.js.json delete mode 100644 tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index a2f399e346..93bf43e953 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -36,7 +36,6 @@ import { tryLowerExpression } from "./try-lower-expression.js"; import { fenceSymbolFieldCopy } from "../symbol-fields.js"; import { planUnionRetag } from "../union-retag.js"; import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js"; -import { builtinGlobalValue } from "./property-access.js"; /** `{ a: 1, b: "x" }` → recordLit. The record type comes from the * contextual type when tsc has one (annotated declarations, arguments, @@ -395,11 +394,9 @@ export function lowerDynObjectLiteral( const lowerValue = (): IrExpr => ts.isMethodDeclaration(prop) ? lowerer.lowerLambda(prop, true) - : ts.isShorthandPropertyAssignment(prop) && shorthandBuiltinValue(lowerer, prop) - ? shorthandBuiltinValue(lowerer, prop)! - : !boxValue && ts.isObjectLiteralExpression(valueExpr) - ? lowerer.lowerExprExpecting(valueExpr, DYN) - : lowerer.lowerExpr(valueExpr as ts.Expression); + : !boxValue && ts.isObjectLiteralExpression(valueExpr) + ? lowerer.lowerExprExpecting(valueExpr, DYN) + : lowerer.lowerExpr(valueExpr as ts.Expression); raw = fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ?? lowerValue(); } catch (err) { // A PURE member read a JS file cannot lower (a namespace object @@ -3157,20 +3154,6 @@ function lowerDeclaredSpreadMerge( /** The value of a shorthand property (`{ x }`): the binding `x` refers to. * The property name's own symbol is the PROPERTY, so resolution goes * through getShorthandAssignmentValueSymbol. */ -/** `{ TypeError }`: a stdlib global in shorthand position reads as the value - * its name reads as elsewhere (the interned constructor or function value), - * or null when the global has no such value. The shorthand's name carries - * the property's symbol, so the identifier path cannot resolve it. */ -function shorthandBuiltinValue( - lowerer: Lowerer, - prop: ts.ShorthandPropertyAssignment, -): IrExpr | null { - const symbol = lowerer.checker.getShorthandAssignmentValueSymbol(prop); - if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; - const js = isJsSourceFile(ts.getSourceFileOfNode(prop)); - return builtinGlobalValue(ts.symbolName(symbol), locOf(prop.name), js); -} - export function lowerShorthandValue( lowerer: Lowerer, prop: ts.ShorthandPropertyAssignment, @@ -3187,8 +3170,6 @@ export function lowerShorthandValue( const callable = lowerer.lowerBuiltinCallableValue(builtin, loc); if (callable) return callable; } - const builtinValue = shorthandBuiltinValue(lowerer, prop); - if (builtinValue) return builtinValue; if (symbol) { if (lowerer.ctx.selfSymbol === symbol) { return { kind: "selfRef", type: lowerer.ctx.selfType!, loc }; diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index d661109509..06186c4dc3 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -4024,26 +4024,10 @@ function lowerStreamObjectProperty( return lowerStreamProperty(lowerer, expr, info); } -/** The error constructors among GLOBAL_CONSTRUCTOR_VALUES. Their statics - * (`Error.stackTraceLimit`, `Error.captureStackTrace`) keep their own - * lowerings, so fenceBuiltinConstructorStaticRead leaves them alone. */ -const ERROR_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ - "Error", - "TypeError", - "RangeError", - "SyntaxError", - "ReferenceError", - "EvalError", - "URIError", - "AggregateError", -]); - /** The builtin constructors JavaScript sources read as real function * values rather than identity tokens: the runtime interns one function per * constructor, and dynamic values answer `.constructor` with the same one, - * so `o.constructor === Object` and `err.constructor === TypeError` are - * exact identity tests, and `err.constructor.name` is the constructor's - * name. */ + * so `o.constructor === Object` is an exact identity test. */ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ "Object", "Function", @@ -4054,7 +4038,6 @@ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ "Promise", "WeakMap", "WeakSet", - ...ERROR_CONSTRUCTOR_VALUES, ]); /** The builtin global functions JavaScript sources read as interned @@ -4197,7 +4180,6 @@ function fenceBuiltinConstructorStaticRead( symbol !== undefined && ts.symbolName(symbol).endsWith("Constructor") && GLOBAL_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) && - !ERROR_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) && lowerer.isStdlibSymbol(symbol) ) lowerer.stdlibMemberFence(access); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 7dc0b87cc1..84c54307ab 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -3889,9 +3889,6 @@ function stdlibGlobalTokenDestructure( if (decl.initializer === undefined || !isJsSourceFile(ts.getSourceFileOfNode(decl))) return null; const globalName = stdlibGlobalNameOf(lowerer, decl.initializer); if (globalName === null) return null; - // `workerData` is a value (the data the worker was started with), not an - // API surface: its members destructure as any untyped value's do. - if (globalName === "workerData") return null; if (!ts.isObjectBindingPattern(decl.name)) { lowerer.unsupported( "SC1031", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json deleted file mode 100644 index a2f0e02ea5..0000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/6870-error-constructor-values.js" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json deleted file mode 100644 index b610a58788..0000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/6871-worker-data-destructuring.cjs" - ], - "diags": [] -} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 1d8a7f1d49..cafc8d07e5 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -725,9 +725,6 @@ ScrDyn *scr_dyn_array_constructor(void); /* The interned builtin constructor `name` (Object, Date, RegExp, Map, Set, * Promise, WeakMap, WeakSet) as a dynamic function value: +1. */ ScrDyn *scr_dyn_global_constructor(const ScrStr *name); -/* The interned function value of an error constructor ("TypeError"), or - * NULL for other names. */ -ScrDyn *scr_dyn_error_constructor(const char *name); /* A builtin global function (setTimeout, setImmediate, process.nextTick, ...) * as an interned dynamic function value: +1. */ ScrDyn *scr_dyn_builtin_function(const ScrStr *name); diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 7b375018b0..2a010f2131 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -1913,8 +1913,6 @@ static ScrDyn *dyn_function_constructor(void) { ScrDyn *scr_dyn_global_constructor(const ScrStr *name) { if (strcmp(name->data, dyn_s20) == 0) return dyn_function_constructor(); - ScrDyn *error = scr_dyn_error_constructor(name->data); - if (error) return error; ScrDyn *ctor = dyn_global_ctor(name->data); if (!ctor) scr_trap(dyn_s3); return ctor; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 57846b0416..194ade8879 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1801,55 +1801,6 @@ static void scr_builtin_object_prototype_cleanup(void) { scr_dyn_release(prototype); } -/* The error constructors as values (an error's `constructor`, and the - * globals in JavaScript): one interned function per constructor and thread, - * indexed like scr_error_vts, with the constructor's name and length. - * Calling one through the value is not supported yet, as for the other - * interned constructors (scr_dyn_global_constructor). */ -static const char *const scr_error_ctor_names[] = { - "Error", "TypeError", "RangeError", "SyntaxError", NULL, - "ReferenceError", "EvalError", "URIError", "AggregateError", -}; -#define SCR_ERROR_CTORS (sizeof scr_error_ctor_names / sizeof scr_error_ctor_names[0]) -static SCR_TL ScrDyn *scr_error_ctor_values[SCR_ERROR_CTORS]; -static SCR_TL bool scr_error_ctors_registered; - -static void scr_error_ctors_cleanup(void) { - for (size_t i = 0; i < SCR_ERROR_CTORS; i++) { - scr_dyn_release(scr_error_ctor_values[i]); - scr_error_ctor_values[i] = NULL; - } -} - -static ScrDyn *scr_error_ctor_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { - (void)args; - (void)argc; - char message[128]; - int length = snprintf(message, sizeof message, - "calling the %s constructor through a stored value is not supported yet", - (const char *)closure->fn); - scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, "SC2020"); - return NULL; -} - -ScrDyn *scr_dyn_error_constructor(const char *name) { - for (size_t i = 0; i < SCR_ERROR_CTORS; i++) { - if (!scr_error_ctor_names[i] || strcmp(scr_error_ctor_names[i], name) != 0) continue; - if (!scr_error_ctors_registered) { - scr_error_ctors_registered = true; - scr_atexit(scr_error_ctors_cleanup); - } - if (!scr_error_ctor_values[i]) { - uint32_t length = strcmp(name, "AggregateError") == 0 ? 2 : 1; - scr_error_ctor_values[i] = - scr_dyn_new_func(scr_closure_new((void *)scr_error_ctor_names[i], 0), scr_error_ctor_call, length, - "native:constructor", scr_error_ctor_names[i]); - } - return scr_dyn_retain(scr_error_ctor_values[i]); - } - return NULL; -} - ScrDyn *scr_dyn_object_prototype(void) { if (!scr_builtin_object_prototype) { scr_builtin_object_prototype = scr_dyn_alloc(SCR_DYN_OBJ); @@ -7756,17 +7707,6 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { return out; } case SCR_DYN_FUNC: { - /* The interned builtin constructors print with their names, as in Node - * (function Map() { [native code] }); other functions without a source - * text print the anonymous native form. */ - if (d->v.fn.sig && strcmp(d->v.fn.sig, "native:constructor") == 0 && d->v.fn.name) { - ScrJsonBuf b; - scr_jb_init(&b); - scr_jb_puts(&b, "function "); - scr_jb_puts(&b, d->v.fn.name); - scr_jb_puts(&b, "() { [native code] }"); - return scr_jb_finish(&b); - } static const char f[] = "function () { [native code] }"; return scr_str_new(f, sizeof f - 1); } @@ -11720,18 +11660,21 @@ static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_ ScrDyn *own = scr_dyn_obj_get(d, key, key_len); if (own) return scr_dyn_retain(own); if (key_len == 11 && memcmp(key, "constructor", 11) == 0) { - /* An error's constructor: the interned function value its global - * reads as (scr_dyn_error_constructor). */ - static const char *const names[] = { - "Error", "TypeError", "RangeError", "SyntaxError", NULL, - "ReferenceError", "EvalError", "URIError", "AggregateError", + static const char *const tokens[] = { + "[builtin Error]", "[builtin TypeError]", + "[builtin RangeError]", "[builtin SyntaxError]", + NULL, "[builtin ReferenceError]", "[builtin EvalError]", "[builtin URIError]", + "[builtin AggregateError]", }; size_t hit = scr_errdyn_find(d, false); const ScrVt *vt = hit == SIZE_MAX ? NULL : scr_errdyn_cache[hit].err->vt; - for (size_t kind = 0; vt && kind < sizeof names / sizeof names[0]; kind++) { - if (!names[kind]) continue; + for (size_t kind = 0; vt && kind < sizeof tokens / sizeof tokens[0]; kind++) { + if (!tokens[kind]) continue; if (vt != &scr_error_vts[kind]) continue; - return scr_dyn_error_constructor(names[kind]); + ScrStr *token = scr_str_new(tokens[kind], strlen(tokens[kind])); + ScrDyn *result = scr_dyn_new_str(token); + scr_str_release(token); + return result; } } return scr_dyn_retain(scr_dyn_undefined()); diff --git a/tests/corpus/6870-error-constructor-values.js b/tests/corpus/6870-error-constructor-values.js deleted file mode 100644 index 81f5086d28..0000000000 --- a/tests/corpus/6870-error-constructor-values.js +++ /dev/null @@ -1,55 +0,0 @@ -// The error constructors as values in JavaScript: an error's `constructor` -// is the same function the global names, whose name, length, typeof and -// String() are Node's, for errors the program throws, errors the runtime -// throws, and errors read back from catch bindings and containers. -function describe(label, error) { - const ctor = error.constructor; - console.log(label, error.name, error.constructor.name, typeof ctor, ctor.length, String(ctor)); -} - -describe('Error', new Error('e')); -describe('TypeError', new TypeError('t')); -describe('RangeError', new RangeError('r')); -describe('SyntaxError', new SyntaxError('s')); -describe('ReferenceError', new ReferenceError('f')); -describe('EvalError', new EvalError('v')); -describe('URIError', new URIError('u')); -describe('AggregateError', new AggregateError([], 'a')); - -const thrown = [ - () => JSON.parse('{'), - () => { - const holder = JSON.parse('{"value":null}'); - return holder.value.missing; - }, - () => new Array(-1), - () => decodeURIComponent('%'), - () => { - throw new TypeError('own'); - }, -]; -const constructors = { SyntaxError, TypeError, RangeError, URIError }; -const caught = []; -for (const run of thrown) { - try { - run(); - } catch (error) { - caught.push(error); - console.log('caught', error.constructor.name, error.constructor === constructors[error.name]); - } -} - -const byName = new Map(); -for (const error of caught) byName.set(error.constructor.name, (byName.get(error.constructor.name) ?? 0) + 1); -console.log('kinds', JSON.stringify([...byName])); - -console.log('identity', new TypeError('x').constructor === TypeError, new RangeError('x').constructor === TypeError); -console.log('globals', Error.name, TypeError.length, AggregateError.length, typeof RangeError, String(SyntaxError)); - -const renamed = new TypeError('x'); -renamed.name = 'Custom'; -console.log('renamed', renamed.name, renamed.constructor.name); - -class ParseFailure extends SyntaxError {} -const failure = new ParseFailure('p'); -console.log('subclass', failure.name, failure.constructor.name, failure instanceof SyntaxError); diff --git a/tests/corpus/6871-worker-data-destructuring.cjs b/tests/corpus/6871-worker-data-destructuring.cjs deleted file mode 100644 index 63ae789bdf..0000000000 --- a/tests/corpus/6871-worker-data-destructuring.cjs +++ /dev/null @@ -1,30 +0,0 @@ -// Destructuring the members of a worker's workerData in JavaScript: plain, -// renamed, defaulted, nested and rest bindings read the data the worker was -// started with, as reading `workerData.` does. -'use strict'; -const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads'); - -if (isMainThread) { - const sab = new SharedArrayBuffer(8); - const data = { name: 'job-7', size: 3, options: { verbose: true, tags: ['a', 'b'] }, counters: new Int32Array(sab), extra: 1, more: 'x' }; - const worker = new Worker(__filename, { workerData: data }); - worker.on('message', (lines) => { - for (const line of lines) console.log(line); - console.log('main sees', new Int32Array(sab)[0]); - }); -} else { - const lines = []; - const { name, size } = workerData; - lines.push(`plain ${name} ${size}`); - const { name: jobName, missing = 'fallback', size: count = 10 } = workerData; - lines.push(`renamed ${jobName} ${missing} ${count}`); - const { - options: { verbose, tags: [firstTag, ...otherTags] }, - } = workerData; - lines.push(`nested ${verbose} ${firstTag} ${JSON.stringify(otherTags)}`); - const { name: ignored, size: alsoIgnored, options, counters, ...rest } = workerData; - lines.push(`rest ${JSON.stringify(rest)} ${typeof options} ${ignored === name && alsoIgnored === size}`); - counters[0] = 41; - counters[0] += 1; - parentPort.postMessage(lines); -} diff --git a/tests/harness/corpus-costs/6870-error-constructor-values.js.json b/tests/harness/corpus-costs/6870-error-constructor-values.js.json deleted file mode 100644 index c608ce33a4..0000000000 --- a/tests/harness/corpus-costs/6870-error-constructor-values.js.json +++ /dev/null @@ -1 +0,0 @@ -{ "plain": 1200, "san": 3400 } diff --git a/tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json b/tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json deleted file mode 100644 index d6c0cb9eea..0000000000 --- a/tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json +++ /dev/null @@ -1 +0,0 @@ -{ "plain": 1300, "san": 3490 } From 4919576d097984a23f6005cf52d9fb33bda2b6a3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 04:16:06 -0500 Subject: [PATCH 096/395] Filter regex start positions by their leading character sets --- .../src/backend/llvm/regex-matchers.test.ts | 19 ++ .../src/backend/llvm/regex-matchers.ts | 163 +++++++++++++++++- .../corpus/6872-regex-start-filter.ts.json | 6 + tests/corpus/6872-regex-start-filter.ts | 79 +++++++++ .../6872-regex-start-filter.ts.json | 1 + 5 files changed, 262 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6872-regex-start-filter.ts.json create mode 100644 tests/corpus/6872-regex-start-filter.ts create mode 100644 tests/harness/corpus-costs/6872-regex-start-filter.ts.json diff --git a/packages/compiler/src/backend/llvm/regex-matchers.test.ts b/packages/compiler/src/backend/llvm/regex-matchers.test.ts index 1acd9c465c..d009de7c9d 100644 --- a/packages/compiler/src/backend/llvm/regex-matchers.test.ts +++ b/packages/compiler/src/backend/llvm/regex-matchers.test.ts @@ -110,6 +110,25 @@ describe("regex literal matcher code", () => { expect(body("a\\d{1,3}")).not.toContain("<16 x i8>"); }); + test("fixed leading characters filter 16 start positions at once", () => { + // Both alternatives' first characters are tested at every offset and + // combined, and the reads reach seven characters past the block. + const text = body("agggtaaa|tttaccct", "gi"); + expect(text.match(/load <16 x i8>/g)!.length).toBeGreaterThanOrEqual(16); + expect(text).toMatch(/and i16 /); + expect(text).toMatch(/or i16 /); + expect(text).toMatch(/add i32 %t\d+, 7\n/); + // A class sequence with a fixed count extends the filter. + expect(body("[cg]x{3}y")).toMatch(/and i16 /); + // An optional first character, a single first character and a + // variable-length start keep the first-character search. + expect(body("[ab]?cd")).not.toMatch(/and i16 /); + expect(body("[ab]*cd")).not.toMatch(/and i16 /); + expect(body("xy|zw")).toMatch(/and i16 /); + expect(body("[ab]|cd")).not.toMatch(/and i16 /); + expect(body("agggtaaa|tttaccct", "gi", false)).not.toContain("<16 x i8>"); + }); + test("loops no continuation can share push no backtrack entries", () => { // \d+ then a dot; [^\]]+ then ]; the run at the end of the pattern. expect(body("\\d+\\.\\d+")).not.toContain("@scr_regex_stack_grow"); diff --git a/packages/compiler/src/backend/llvm/regex-matchers.ts b/packages/compiler/src/backend/llvm/regex-matchers.ts index 001f081e5f..77f8a06ebc 100644 --- a/packages/compiler/src/backend/llvm/regex-matchers.ts +++ b/packages/compiler/src/backend/llvm/regex-matchers.ts @@ -589,6 +589,90 @@ function firstOf(node: RxNode): FirstInfo { } } +/** Positions the start filter tests and alternatives it keeps. */ +const PREFIX_LENGTH = 8; +const PREFIX_ALTERNATIVES = 8; + +/** The character sets a match's first characters must belong to: every + * match of the node begins with the characters of some entry of `alts`, + * one set per character. `complete` means every match is exactly as long as + * its entry (so a following node continues the sequences). Null when the + * node gives no usable condition (an alternative that can start with + * anything, or too many alternatives). Assertions add no characters: the + * result stays a necessary condition. */ +interface Prefix { + alts: CharSet[][]; + complete: boolean; +} + +/** Each head followed by each tail, cut to PREFIX_LENGTH sets; null past + * PREFIX_ALTERNATIVES entries. */ +function crossPrefixes(heads: CharSet[][], tails: CharSet[][]): CharSet[][] | null { + if (heads.length * tails.length > PREFIX_ALTERNATIVES) return null; + const out: CharSet[][] = []; + for (const head of heads) + for (const tail of tails) out.push([...head, ...tail].slice(0, PREFIX_LENGTH)); + return out; +} + +/** Whether no entry was cut at PREFIX_LENGTH. */ +function uncut(alts: CharSet[][]): boolean { + return alts.every((alt) => alt.length < PREFIX_LENGTH); +} + +function prefixOf(node: RxNode): Prefix | null { + switch (node.kind) { + case "set": + return { alts: [[node.set]], complete: true }; + case "assert": + return { alts: [[]], complete: true }; + case "cap": + return prefixOf(node.body); + case "alt": { + const alts: CharSet[][] = []; + let complete = true; + for (const alt of node.alts) { + const p = prefixOf(alt); + if (p === null) return null; + alts.push(...p.alts); + complete &&= p.complete; + } + return alts.length <= PREFIX_ALTERNATIVES ? { alts, complete } : null; + } + case "seq": { + let alts: CharSet[][] = [[]]; + for (const item of node.items) { + const p = prefixOf(item); + if (p === null) return { alts, complete: false }; + const joined = crossPrefixes(alts, p.alts); + if (joined === null) return { alts, complete: false }; + alts = joined; + if (!p.complete || !uncut(alts)) return { alts, complete: false }; + } + return { alts, complete: true }; + } + case "rep": { + if (node.max === 0) return { alts: [[]], complete: true }; + if (node.min === 0) return { alts: [[]], complete: false }; + const body = prefixOf(node.body); + if (body === null) return null; + // The first `min` iterations are required; they extend the sequences + // while the body has a fixed length. + let alts = body.alts; + let covered = 1; + while (covered < node.min && body.complete && uncut(alts)) { + const joined = crossPrefixes(alts, body.alts); + if (joined === null) break; + alts = joined; + covered++; + } + const complete = + body.complete && covered === node.min && node.min === node.max && uncut(alts); + return { alts, complete }; + } + } +} + /** Whether a node can match without consuming (assertions included). */ function canMatchEmpty(node: RxNode): boolean { switch (node.kind) { @@ -721,16 +805,74 @@ class RxGen { return wide; } + /** The vector plans of a start filter (prefixOf): per alternative, the + * plans of its first sets up to the first set without one. Null unless + * every alternative keeps at least two positions, where the filter is + * more selective than the first character alone. */ + prefixPlans(prefix: Prefix | null): { ranges: number[][]; negate: boolean }[][] | null { + if (prefix === null || !this.vector) return null; + const plans: { ranges: number[][]; negate: boolean }[][] = []; + for (const alt of prefix.alts) { + const row: { ranges: number[][]; negate: boolean }[] = []; + for (const set of alt) { + const plan = this.vectorPlan(set); + if (plan === null) break; + row.push(plan); + } + if (row.length < 2) return null; + plans.push(row); + } + return plans.length === 0 ? null : plans; + } + + /** Start positions s[index..index+16) where some alternative of the + * filter matches its first sets: an i16 mask. */ + testPrefix(plans: { ranges: number[][]; negate: boolean }[][], index: string): string { + let any = ""; + for (const row of plans) { + let all = ""; + for (let k = 0; k < row.length; k++) { + let at = index; + if (k > 0) { + at = this.tmp(); + this.line(`${at} = add i32 ${index}, ${k}`); + } + const bits = this.testVector(row[k]!, at); + if (all === "") all = bits; + else { + const both = this.tmp(); + this.line(`${both} = and i16 ${all}, ${bits}`); + all = both; + } + } + if (any === "") any = all; + else { + const either = this.tmp(); + this.line(`${either} = or i16 ${any}, ${all}`); + any = either; + } + } + return any; + } + /** The first-character search: from %at, store the next position whose - * character is in `set` and branch to %attempt, or branch to %nomatch. */ - emitSeek(set: CharSet): void { + * character is in `set` and branch to %attempt, or branch to %nomatch. + * With a start filter (`prefix`), the vector loop tests the filter's + * first sets of each alternative at 16 start positions at once and stops + * only where one of them matches all of its sets; a match can start + * nowhere else, so the attempts that remain are the ones that can + * succeed, in the same order. */ + emitSeek(set: CharSet, prefix: Prefix | null = null): void { this.block("seek"); const a = this.tmp(); this.line(`${a} = load i32, ptr %at`); + const filter = this.prefixPlans(prefix); const plan = this.vectorPlan(set); + // The filter reads s[i + reach + 15] at its farthest. + const reach = filter === null ? 0 : Math.max(...filter.map((row) => row.length)) - 1; let from = "seek", start = a; - if (plan !== null) { + if (plan !== null || filter !== null) { const head = this.label("vsh"), load = this.label("vsl"), next = this.label("vsn"), @@ -743,10 +885,16 @@ class RxGen { fits = this.tmp(); this.line(`${i} = phi i32 [ ${a}, %seek ], [ ${i16}, %${next} ]`); this.line(`${i16} = add i32 ${i}, 16`); - this.line(`${fits} = icmp sle i32 ${i16}, %len`); + if (reach === 0) { + this.line(`${fits} = icmp sle i32 ${i16}, %len`); + } else { + const last = this.tmp(); + this.line(`${last} = add i32 ${i16}, ${reach}`); + this.line(`${fits} = icmp sle i32 ${last}, %len`); + } this.line(`br i1 ${fits}, label %${load}, label %${tail}`); this.block(load); - const mask = this.testVector(plan, i), + const mask = filter !== null ? this.testPrefix(filter, i) : this.testVector(plan!, i), any = this.tmp(); this.line(`${any} = icmp ne i16 ${mask}, 0`); this.line(`br i1 ${any}, label %${hit}, label %${next}`); @@ -1769,7 +1917,10 @@ export function compileRegexMatcher( fn.push(` store i32 %off.n, ptr %at`); fn.push(` br label %attempt`); } else { - gen.emitSeek(first.set); + // A start filter over the first few characters (a single first byte + // keeps memchr). + const prefix = prefixOf(root); + gen.emitSeek(first.set, gen.prefixPlans(prefix) === null ? null : prefix); } } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6872-regex-start-filter.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6872-regex-start-filter.ts.json new file mode 100644 index 0000000000..e2c97c71fc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6872-regex-start-filter.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6872-regex-start-filter.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6872-regex-start-filter.ts b/tests/corpus/6872-regex-start-filter.ts new file mode 100644 index 0000000000..872342e934 --- /dev/null +++ b/tests/corpus/6872-regex-start-filter.ts @@ -0,0 +1,79 @@ +// Searches whose patterns begin with fixed sequences of characters and +// classes (literal alternations, classes, fixed repetitions), over subjects +// of every length from 0 to 80 and with matches at every offset: match, +// matchAll, search, exec with captures, global and sticky lastIndex, +// replace, case-insensitive letters, multiline anchors and word boundaries. +// Results must not depend on where a match falls relative to 16-character +// blocks or the end of the subject. +const patterns: RegExp[] = [ + /agggtaaa|tttaccct/gi, + /[cgt]gggtaaa|tttaccc[acg]/gi, + /ab|abc|b/g, + /(ab)(c|d)x/g, + /x[0-9]{3}y/g, + /(?:no|yes)(!+)/gi, + /^ab/gm, + /\bcat\b/g, + /a{2}(b+)c/g, + /[A-Z][a-z]{2}-[0-9]/g, +]; + +function describe(re: RegExp, subject: string): string { + const parts: string[] = []; + const all = subject.match(re); + parts.push(`match=${all === null ? "null" : JSON.stringify(all)}`); + parts.push(`search=${subject.search(re)}`); + const rows: string[] = []; + for (const m of subject.matchAll(re)) rows.push(`${m.index}:${JSON.stringify(m.slice(0))}`); + parts.push(`all=[${rows.join(" ")}]`); + parts.push(`replace=${JSON.stringify(subject.replace(re, "<$&>"))}`); + return parts.join(" "); +} + +const filler = "qwertyuiopasdfghjklzxcvbnm"; +const needles = ["agggtaaa", "TTTACCCT", "tttacccg", "abcx", "abdx", "x123y", "NO!!", "yes!", "cat", "aabbbc", "Abc-1", "ab"]; +let checksum = 0; +let lines = 0; +for (let length = 0; length <= 80; length += 7) { + for (const needle of needles) { + for (const offset of [0, 1, 14, 15, 16, 17, 31, 32, 33]) { + if (offset + needle.length > length) continue; + let subject = ""; + while (subject.length < length) subject += filler; + subject = subject.slice(0, offset) + needle + subject.slice(offset + needle.length, length); + for (const re of patterns) { + const text = describe(re, subject); + for (let i = 0; i < text.length; i++) checksum = (checksum * 31 + text.charCodeAt(i)) >>> 0; + lines++; + if (length === 35 && (offset === 15 || offset === 16)) console.log(re.source, offset, needle, text); + } + } + } +} +console.log("lines", lines, "checksum", checksum); + +// Overlapping candidates: every start position looks like a match for the +// first characters, and only some complete. +const dense = "agggtaa".repeat(9) + "agggtaaa" + "tttaccc".repeat(5) + "tttaccct"; +console.log(describe(/agggtaaa|tttaccct/g, dense)); + +// exec with lastIndex, global and sticky. +const re = /x[0-9]{3}y/g; +const text = "zzx111yzzzzzzzzzzzzzzzzzzx222yx333y" + "z".repeat(20) + "x444y"; +let m: RegExpExecArray | null; +const found: string[] = []; +while ((m = re.exec(text)) !== null) found.push(`${m[0]}@${m.index}->${re.lastIndex}`); +console.log("exec", found.join(" ")); +const sticky = /x[0-9]{3}y/y; +sticky.lastIndex = 2; +console.log("sticky", sticky.test(text), sticky.lastIndex, sticky.test(text), sticky.lastIndex); + +// Short subjects and the tail. +for (const subject of ["", "a", "agggtaa", "agggtaaa", "zagggtaaa", "z".repeat(16) + "agggtaaa", "z".repeat(23) + "tttaccct"]) { + console.log(JSON.stringify(subject.slice(-12)), subject.length, describe(/agggtaaa|tttaccct/gi, subject)); +} + +// A long subject with matches spread through it. +let long = ""; +for (let i = 0; i < 2000; i++) long += i % 97 === 0 ? "aGgGtAaA" : i % 89 === 0 ? "tttACCCt" : filler[i % filler.length]; +console.log("long", (long.match(/agggtaaa|tttaccct/gi) ?? []).length, long.search(/tttaccct/i)); diff --git a/tests/harness/corpus-costs/6872-regex-start-filter.ts.json b/tests/harness/corpus-costs/6872-regex-start-filter.ts.json new file mode 100644 index 0000000000..c9d5dd2648 --- /dev/null +++ b/tests/harness/corpus-costs/6872-regex-start-filter.ts.json @@ -0,0 +1 @@ +{ "plain": 2430, "san": 8860 } From 67247dd26bb8af65349e69c73ffa80603254632c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 04:21:55 -0500 Subject: [PATCH 097/395] Support objectMode streams with any value as a chunk Readable, Writable, Duplex, Transform and PassThrough take the objectMode option, and a Duplex, Transform or PassThrough the readableObjectMode and writableObjectMode options, as true or false literals in an options object literal or in an options value. An object-mode side holds any value that converts to a checked-dynamic value: push, unshift, write, end, pipe, pipeline, for await, 'data' listeners, Readable.from and the transform callback carry values in TypeScript and JavaScript, and constructor-function streams and options held in variables do the same. The object-mode high-water mark counts values and defaults to 16. A readable buffer that mixes strings, Buffers and other values holds them all, and push(undefined) and other values on a byte-mode stream report ERR_INVALID_ARG_TYPE through 'error', as in Node. In typed code a write or transform callback, or a _write or _transform method, may declare its chunk with any such type, which converts when the callback runs. Writes pass the encoding argument to _write and _transform as Node does: undefined when absent, null when a callback took its place, otherwise the value unchanged. A parameter declared string | null | undefined receives that value and one declared string receives "" for undefined and null. write(chunk, encoding) on a byte-mode stream encodes a string chunk with any known encoding and throws ERR_UNKNOWN_ENCODING for an unknown one. Readable.from takes typed Maps and Sets of any such value, and arrays that mix strings and Buffers. for await over a readable-only stream that will emit 'close' now ends after 'close', as Node's eos does. Buffered writes that complete inside clearBuffer run their callbacks in order, and 'finish' waits for a deferred afterWrite that carries a callback, as Node's pendingcb does, unless a _final callback schedules it. The 'data' listener adapters take three slots and write and transform callbacks read their chunk and encoding through runtime accessors, so RUNTIME_ABI_VERSION is now 26. --- .../builtin-apis/streams-and-readline.mdx | 10 +- .../limitations/builtin-function-values.mdx | 2 +- packages/cli/test/native-link-info.test.ts | 4 +- .../ambient/scriptc-node-fallback.d.ts | 85 +- .../src/backend/llvm/expr-stream-callbacks.ts | 217 +++- .../compiler/src/backend/llvm/lib-process.ts | 37 +- .../compiler/src/backend/llvm/lib-shared.ts | 5 + .../compiler/src/backend/llvm/lib-stream.ts | 52 +- 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 +- .../frontend/lowering/lower-event-emitter.ts | 51 +- .../src/frontend/lowering/lower-stream.ts | 323 ++++-- packages/compiler/src/ir/builtin-effects.ts | 2 + .../compiler/src/ir/builtin-signatures.ts | 14 +- packages/compiler/src/ir/runtime-features.ts | 2 + packages/compiler/src/ir/validate.ts | 9 +- .../test/native-codegen-integration.test.ts | 4 +- .../corpus/7240-objectmode-streams.ts.json | 6 + .../corpus/7241-objectmode-streams.mjs.json | 6 + .../corpus/7242-objectmode-dynamic.cjs.json | 6 + .../7243-for-await-close-order.mjs.json | 6 + .../corpus/7244-objectmode-subclasses.ts.json | 6 + .../7245-writable-callback-order.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 | 58 +- packages/runtime/src/scr_stream.c | 951 +++++++++++++----- packages/runtime/src/scr_stream.h | 25 + packages/runtime/src/scr_stream_ops.c | 12 +- tests/corpus/7240-objectmode-streams.ts | 130 +++ tests/corpus/7241-objectmode-streams.mjs | 148 +++ tests/corpus/7242-objectmode-dynamic.cjs | 93 ++ tests/corpus/7243-for-await-close-order.mjs | 58 ++ tests/corpus/7244-objectmode-subclasses.ts | 79 ++ tests/corpus/7245-writable-callback-order.mjs | 71 ++ .../7240-objectmode-streams.ts.json | 1 + .../7241-objectmode-streams.mjs.json | 1 + .../7242-objectmode-dynamic.cjs.json | 1 + .../7243-for-await-close-order.mjs.json | 1 + .../7244-objectmode-subclasses.ts.json | 1 + .../7245-writable-callback-order.mjs.json | 1 + .../self-hosting-native-toolchain.test.ts | 2 +- 46 files changed, 2064 insertions(+), 442 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7240-objectmode-streams.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7241-objectmode-streams.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7242-objectmode-dynamic.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7243-for-await-close-order.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7244-objectmode-subclasses.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7245-writable-callback-order.mjs.json create mode 100644 tests/corpus/7240-objectmode-streams.ts create mode 100644 tests/corpus/7241-objectmode-streams.mjs create mode 100644 tests/corpus/7242-objectmode-dynamic.cjs create mode 100644 tests/corpus/7243-for-await-close-order.mjs create mode 100644 tests/corpus/7244-objectmode-subclasses.ts create mode 100644 tests/corpus/7245-writable-callback-order.mjs create mode 100644 tests/harness/corpus-costs/7240-objectmode-streams.ts.json create mode 100644 tests/harness/corpus-costs/7241-objectmode-streams.mjs.json create mode 100644 tests/harness/corpus-costs/7242-objectmode-dynamic.cjs.json create mode 100644 tests/harness/corpus-costs/7243-for-await-close-order.mjs.json create mode 100644 tests/harness/corpus-costs/7244-objectmode-subclasses.ts.json create mode 100644 tests/harness/corpus-costs/7245-writable-callback-order.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 4e0832c261..8461cc71c8 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -1,13 +1,17 @@ -- **Streams and readline:** Stream chunks are Buffers or strings. +- **Streams and readline:** Stream chunks are Buffers or strings, and on an object-mode side any value that converts to a checked-dynamic value. `NodeJS.ReadableStream` and `NodeJS.WritableStream`, and `stream.Readable` and `stream.Writable` under `@types/node`, accept every stream: when a program passes file, zlib, or `node:stream` streams through them, child-process pipes and `process.stdout` and `process.stderr` join those streams as `Readable` and `Writable` instances. `readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream. When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects. Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol. A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error. Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one. - `Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported. + `Readable.from` accepts strings, Buffers, arrays, Sets, Maps, Map and Set iterators, generators, async generators, and class iterables and async iterables, and in JavaScript any iterable or async iterable. In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported. - Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported. + The `objectMode` option, and the `readableObjectMode` and `writableObjectMode` options of a `Duplex`, `Transform`, or `PassThrough`, take `true` or `false` literals in an options object literal; options passed as a value are read when the stream is built. + In typed code, a `write` or `transform` callback, or a `_write` or `_transform` method, may declare its chunk with any type that converts to a checked-dynamic value: the chunk converts when the callback runs, and a value of another type throws a TypeError there. + An object-mode write passes its encoding argument to `write` and `transform` as Node does (`undefined` when absent, `null` when a callback took its place); a parameter declared `string` receives `""` for `undefined` and `null`, and one declared `string | null | undefined` receives Node's value. + `readable.read()` on an object-mode stream answers its Buffers; another value throws a not-supported error (read such values with `'data'` listeners, `for await`, or `pipe`). + `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. 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. diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index 37ed8eb488..c1bab6680b 100644 --- a/docs/content/limitations/builtin-function-values.mdx +++ b/docs/content/limitations/builtin-function-values.mdx @@ -28,7 +28,7 @@ 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 (`objectMode`, `decodeStrings: false`, `signal`, and others) are refused here too, as are `_writev`, `_construct`, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, strings written in encodings other than UTF-8, and pipes into a destination that overrides `write()` or `end()`. +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()`. 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 56ca468788..630ef71733 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: 25, marker: "scr_runtime_abi_v25" }, + runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" }, 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_v25"); + expect(error?.stderr).toContain("scr_runtime_abi_v26"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 936283d407..0d6c3eb1ea 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -6710,11 +6710,10 @@ declare module "node:events" { * write(chunk, enc, cb) {} })`, Duplex/Transform/PassThrough, push/read, * flowing vs paused, write/end backpressure + 'drain', pipe/unpipe, and * destroy — with Node's event orderings. Chunks are BYTES (Buffer), with - * utf8 strings converted at push/write like Node; objectMode and - * setEncoding are declared so uses fence at their sites with their own - * words. `class My extends Readable` (the _read override form) is a - * separate feature and fences at the class declaration. The runtime-fired - * events carry per-class FORCED argument tuples ('data' is one Buffer, + * strings converted at push/write like Node, and any value on an + * object-mode side (objectMode, readableObjectMode, writableObjectMode — + * the object-mode constructor overloads come first). The runtime-fired + * events carry per-class FORCED argument tuples ('data' is one chunk, * 'error' one Error, 'pipe'/'unpipe' one Readable, the rest empty) — the * overloads below hand tsc the same contract so unannotated listener * parameters contextually type in .js sources. */ @@ -6820,6 +6819,51 @@ declare module "stream" { signal?: AbortSignal; } + /* Object-mode options (objectMode: true, or a Duplex's or Transform's + * readableObjectMode / writableObjectMode): chunks and completion data + * are any values, and an object-mode write's encoding argument reaches + * write/transform as passed (undefined when absent, null when a + * callback took its place). The constructors take these before the + * byte-mode options, so a byte-mode stream's callbacks keep Buffer + * chunks. */ + type ObjectModeEncoding = string | null | undefined; + type ObjectModeFlag = + | { objectMode: true } + | { readableObjectMode: true } + | { writableObjectMode: true }; + interface ObjectWritableOptions extends Omit { + objectMode: true; + write?( + this: Writable, + chunk: any, + encoding: ObjectModeEncoding, + callback: (error?: Error | null) => void, + ): void; + } + type ObjectDuplexOptions = Omit & { + write?( + this: Duplex, + chunk: any, + encoding: ObjectModeEncoding, + callback: (error?: Error | null) => void, + ): void; + } & ObjectModeFlag; + type ObjectTransformOptions = Omit & { + transform?( + this: Transform, + chunk: any, + encoding: ObjectModeEncoding, + callback: (error?: Error | null, data?: any) => void, + ): void; + flush?(this: Transform, callback: (error?: Error | null, data?: any) => void): void; + write?( + this: Transform, + chunk: any, + encoding: ObjectModeEncoding, + callback: (error?: Error | null) => void, + ): void; + } & ObjectModeFlag; + /* The operators' options: concurrency and highWaterMark bound map's * in-flight callbacks and queued results. */ interface ArrayOptions { @@ -6839,7 +6883,10 @@ declare module "stream" { [Symbol.asyncIterator](): AsyncIterableIterator; iterator(options?: { destroyOnReturn?: boolean }): AsyncIterableIterator; push(chunk: Buffer | string | null, encoding?: string): boolean; + /* An object-mode stream's values. */ + push(chunk: any, encoding?: string): boolean; unshift(chunk: Buffer | string, encoding?: string): void; + unshift(chunk: any, encoding?: string): void; read(size?: number): Buffer | null; pause(): this; resume(): this; @@ -6946,6 +6993,7 @@ declare module "stream" { } class Writable extends EventEmitter { + constructor(opts: ObjectWritableOptions); constructor(opts?: WritableOptions); /* The underscore-method surface (see Readable). */ _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void; @@ -6961,9 +7009,19 @@ declare module "stream" { encoding: string, callback?: (error?: Error | null) => void, ): boolean; + /* An object-mode stream's values (the encoding argument reaches + * _write as passed). */ + write(chunk: any, callback?: (error?: Error | null) => void): boolean; + write( + chunk: any, + encoding: ObjectModeEncoding, + callback?: (error?: Error | null) => void, + ): boolean; end(callback?: () => void): this; end(chunk: Buffer | string, callback?: () => void): this; end(chunk: Buffer | string, encoding: string, callback?: () => void): this; + end(chunk: any, callback?: () => void): this; + end(chunk: any, encoding: ObjectModeEncoding, callback?: () => void): this; cork(): void; uncork(): void; setDefaultEncoding(encoding: string): this; @@ -7013,6 +7071,7 @@ 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 { + constructor(opts: ObjectDuplexOptions); constructor(opts?: DuplexOptions); /* The underscore-method surface (see Readable). */ _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void; @@ -7027,9 +7086,19 @@ declare module "stream" { encoding: string, callback?: (error?: Error | null) => void, ): boolean; + /* An object-mode stream's values (the encoding argument reaches + * _write as passed). */ + write(chunk: any, callback?: (error?: Error | null) => void): boolean; + write( + chunk: any, + encoding: ObjectModeEncoding, + callback?: (error?: Error | null) => void, + ): boolean; end(callback?: () => void): this; end(chunk: Buffer | string, callback?: () => void): this; end(chunk: Buffer | string, encoding: string, callback?: () => void): this; + end(chunk: any, callback?: () => void): this; + end(chunk: any, encoding: ObjectModeEncoding, callback?: () => void): this; cork(): void; uncork(): void; setDefaultEncoding(encoding: string): this; @@ -7101,16 +7170,18 @@ declare module "stream" { } class Transform extends Duplex { + constructor(opts: ObjectTransformOptions); constructor(opts?: TransformOptions); /* The underscore-method surface (see Readable). */ _transform( chunk: any, encoding: string, - callback: (error?: Error | null, data?: Buffer | string) => void, + callback: (error?: Error | null, data?: any) => void, ): void; - _flush(callback: (error?: Error | null, data?: Buffer | string) => void): void; + _flush(callback: (error?: Error | null, data?: any) => void): void; } class PassThrough extends Transform { + constructor(opts: ObjectTransformOptions); constructor(opts?: TransformOptions); } diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 9be757a20f..17507e1280 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -15,23 +15,55 @@ export function streamDataAdapter( sym = `sc_ee_dad_${host.resolveThunks.size}`; host.resolveThunks.set(key, sym); const p = cbT.params[0]; - if ( - cbT.params.length > 1 || - (p && p.kind !== "bytes" && p.kind !== "string" && p.kind !== "dyn" && p.kind !== "union") - ) { + if (cbT.params.length > 1) { throw new InternalCompilerError( "llvm emitter bug: stream data listener param shape (frontend must fence)", ); } + // A `Buffer | string` union wraps the filled slot; any other union (or + // other type) converts from the chunk's checked-dynamic value. + const bytesOrString = + p?.kind === "union" && + (() => { + const def = host.unionsById.get(p.unionId); + return ( + def !== undefined && + !host.nullableUnions.has(p.unionId) && + def.arms.length === 2 && + def.arms.some((a) => a.kind === "bytes" && a.elem === "u8") && + def.arms.some((a) => a.kind === "string") + ); + })(); host.declare(`declare ptr @scr_box_get_ref(ptr)`); host.declare(`declare void @scr_closure_release(ptr)`); + // The emit ABI's three slots (scr_stream_emit_data): the bytes chunk, + // the string chunk, and an object-mode entry's value. const d: string[] = [ - `define internal void @${sym}(ptr %cb, ptr %a0, ptr %a1) ${FN_ATTRS} { ; stream 'data' adapter ${typeKey(cbT)}`, + `define internal void @${sym}(ptr %cb, ptr %a0, ptr %a1, ptr %a2) ${FN_ATTRS} { ; stream 'data' adapter ${typeKey(cbT)}`, `entry:`, ` %capsp = getelementptr inbounds %ScrClosure, ptr %cb, i64 1`, ` %bx = load ptr, ptr %capsp`, ` %orig = call ptr @scr_box_get_ref(ptr %bx) ; the listener, +1`, ]; + const finishTyped = (arg: string, argTy: string): void => { + d.push( + ` %fnp = getelementptr inbounds %ScrClosure, ptr %orig, i64 0, i32 1`, + ` %fn = load ptr, ptr %fnp`, + ); + const retTy = host.llType(cbT.ret); + const argList = `ptr %orig, ${argTy} ${arg}`; + if (retTy === "void") { + d.push(` call void %fn(${argList})`); + } else { + d.push(` %ret = call ${retTy} %fn(${argList})`); + if (isRefCounted(cbT.ret)) { + d.push( + ` call void ${releaseSym(host.shapeHost, cbT.ret)}(ptr %ret) ; discarded listener result`, + ); + } + } + d.push(` call void @scr_closure_release(ptr %orig)`, ` ret void`); + }; const finish = (arg: string | null): void => { d.push( ` %fnp = getelementptr inbounds %ScrClosure, ptr %orig, i64 0, i32 1`, @@ -57,8 +89,8 @@ export function streamDataAdapter( } else if (p.kind === "bytes" || p.kind === "string") { const msg = p.kind === "bytes" - ? "a 'data' listener declaring a Buffer chunk received a string (the stream has an encoding set)" - : "a 'data' listener declaring a string chunk received a Buffer (call setEncoding, or declare the chunk as a Buffer)"; + ? "a 'data' listener declaring a Buffer chunk received a string or an object-mode value (the stream has an encoding set, or is in object mode)" + : "a 'data' listener declaring a string chunk received a Buffer or an object-mode value (call setEncoding, or declare the chunk's type)"; const slot = p.kind === "bytes" ? "%a0" : "%a1"; host.declare(`declare void @scr_throw_error_msg(i32, ptr, ${host.sizeType})`); d.push( @@ -73,7 +105,7 @@ export function streamDataAdapter( ); finish("%r0"); d.push(`}`, ``); - } else if (p.kind === "union") { + } else if (p.kind === "union" && bytesOrString) { // A `Buffer | string` chunk: wrap whichever slot the stream filled at // its arm of the (boxed) union. const def = host.unionsById.get(p.unionId); @@ -114,11 +146,20 @@ export function streamDataAdapter( finish("%uv"); d.push(`}`, ``); } else { - // dyn: box by runtime tag — the JS lane's adapter parameter. + // dyn: the object-mode value, else box by runtime tag — the JS lane's + // adapter parameter. Any other type converts from that value. host.declare(`declare ptr @scr_dyn_new_buffer(ptr)`); host.declare(`declare ptr @scr_dyn_new_str(ptr)`); + host.declare(`declare ptr @scr_dyn_retain_v(ptr)`); d.push( ` %dslot = alloca ptr`, + ` %isv = icmp ne ptr %a2, null`, + ` br i1 %isv, label %val, label %chunk`, + `val:`, + ` %dvv = call ptr @scr_dyn_retain_v(ptr %a2)`, + ` store ptr %dvv, ptr %dslot`, + ` br label %go`, + `chunk:`, ` %isb = icmp ne ptr %a0, null`, ` br i1 %isb, label %buf, label %str`, `buf:`, @@ -132,7 +173,25 @@ export function streamDataAdapter( `go:`, ` %dv = load ptr, ptr %dslot`, ); - finish("%dv"); + if (p.kind === "dyn") { + finish("%dv"); + } else { + const helper = host.dyn.dynCheckHelper(p, false); + const ty = host.llType(p); + host.declare(`declare void @scr_dyn_release(ptr)`); + host.declare(`declare zeroext i1 @scr_exc_pending()`); + d.push( + ` %tv = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %dv, ptr null)`, + ` call void @scr_dyn_release(ptr %dv)`, + ` %pend = call zeroext i1 @scr_exc_pending()`, + ` br i1 %pend, label %fail, label %conv`, + `fail:`, + ` call void @scr_closure_release(ptr %orig)`, + ` ret void`, + `conv:`, + ); + finishTyped("%tv", ty); + } d.push(`}`, ``); } host.resolveThunkDefs.push(...d); @@ -164,12 +223,25 @@ export function streamDoneFnFor( const dataT = kind === "t" || kind === "l" ? doneT.params[1] : undefined; let bytesTag = -1; let strTag = -1; + // data of any type other than a union of Buffer, string, undefined and + // null (an object-mode Transform's value) completes through the + // value entry points with the data boxed. + let valueData = false; if (dataT !== undefined) { - if (dataT.kind !== "union") - throw new InternalCompilerError("llvm emitter bug: stream done data param not a union"); - const def = host.unionsById.get(dataT.unionId); - bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes") : -1; - strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1; + const def = dataT.kind === "union" ? host.unionsById.get(dataT.unionId) : undefined; + valueData = + def === undefined || + !def.arms.every( + (a) => + (a.kind === "bytes" && a.elem === "u8") || + a.kind === "string" || + a.kind === "undefinedT" || + a.kind === "nullT", + ); + if (!valueData) { + bytesTag = def!.arms.findIndex((a) => a.kind === "bytes"); + strTag = def!.arms.findIndex((a) => a.kind === "string"); + } } const entry = kind === "w" @@ -179,10 +251,16 @@ export function streamDoneFnFor( : kind === "d" ? "scr_stream_destroy_done" : kind === "t" - ? "scr_stream_transform_done" - : "scr_stream_flush_done"; - const twoSlot = kind === "t" || kind === "l"; - host.declare(`declare void @${entry}(ptr, ptr${twoSlot ? ", ptr, ptr" : ""})`); + ? 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" : ""})`, + ); host.declare(`declare ptr @scr_box_get_ref(ptr)`); host.declare(`declare void @scr_stream_release_v(ptr)`); host.declare(`declare ptr @scr_error_retain_v(ptr)`); @@ -190,7 +268,7 @@ export function streamDoneFnFor( const params = [ "ptr %self", ...(errT !== undefined ? ["ptr %e"] : []), - ...(dataT !== undefined ? ["ptr %d"] : []), + ...(dataT !== undefined ? [`${host.llType(dataT)} %d`] : []), ]; const d: string[] = [ `define internal void @${sym}(${params.join(", ")}) ${FN_ATTRS} { ; stream '${kind}' done ${typeKey(doneT)}`, @@ -234,7 +312,15 @@ export function streamDoneFnFor( unwrap("%d", strTag, "@scr_str_retain_v", "%sslot"); } d.push(` %ev = load ptr, ptr %eslot`); - if (twoSlot) { + if (valueData) { + const conv = host.dyn.toDynHelper(dataT!); + host.declare(`declare void @scr_dyn_release(ptr)`); + d.push( + ` %dv = call ptr @${conv}(${host.llType(dataT!)} %d)`, + ` call void @${entry}(ptr %s, ptr %ev, ptr %dv) ; moves err; borrows s and the value`, + ` call void @scr_dyn_release(ptr %dv)`, + ); + } else if (twoSlot) { d.push( ` %bv = load ptr, ptr %bslot`, ` %sv = load ptr, ptr %sslot`, @@ -245,7 +331,8 @@ export function streamDoneFnFor( } d.push(` call void @scr_stream_release_v(ptr %s)`); if (errT !== undefined) d.push(` call void ${releaseSym(host.shapeHost, errT)}(ptr %e)`); - if (dataT !== undefined) d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`); + if (dataT !== undefined && isRefCounted(dataT)) + d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`); d.push(` ret void`, `}`, ``); host.resolveThunkDefs.push(...d); return sym; @@ -611,18 +698,14 @@ export function streamCbThunkFor( : pos === 1; if (p.kind === "dyn") { if (isChunkPos) { - host.declare(`declare ptr @scr_dyn_new_buffer(ptr)`); - d.push(` %dc${i} = call ptr @scr_dyn_new_buffer(ptr %chunk)`); + // the chunk's value: a Buffer, or an object-mode side's value + host.declare(`declare ptr @scr_stream_chunk_value(ptr, ptr)`); + d.push(` %dc${i} = call ptr @scr_stream_chunk_value(ptr %s, ptr %chunk)`); passed.push(`ptr %dc${i}`); } else if (isEncPos) { - host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`); - host.declare(`declare ptr @scr_dyn_new_str(ptr)`); - host.declare(`declare void @scr_str_release(ptr)`); - d.push( - ` %es${i} = call ptr @scr_str_new(ptr ${host.cstr("buffer")}, ${host.sizeType} 6)`, - ` %ed${i} = call ptr @scr_dyn_new_str(ptr %es${i})`, - ` call void @scr_str_release(ptr %es${i})`, - ); + // 'buffer', or the encoding argument an object-mode write passed + host.declare(`declare ptr @scr_stream_chunk_encoding_value(ptr)`); + d.push(` %ed${i} = call ptr @scr_stream_chunk_encoding_value(ptr %s)`); passed.push(`ptr %ed${i}`); } else if (isErrPos) { // finished/pipeline succeed with UNDEFINED (Node calls the eos @@ -673,15 +756,71 @@ export function streamCbThunkFor( } continue; } - if (isChunkPos) { - host.declare(`declare ptr @scr_bytes_retain_v(ptr)`); - d.push(` %rc${i} = call ptr @scr_bytes_retain_v(ptr %chunk)`); + if (isChunkPos && p.kind === "bytes") { + // A Buffer, which an object-mode side's value must be (else a + // TypeError, thrown out of the write). + host.declare(`declare ptr @scr_stream_chunk_bytes(ptr, ptr)`); + d.push( + ` %rc${i} = call ptr @scr_stream_chunk_bytes(ptr %s, ptr %chunk)`, + ` %rcbad${i} = icmp eq ptr %rc${i}, null`, + ` br i1 %rcbad${i}, label %rcfail${i}, label %rcok${i}`, + `rcfail${i}:`, + ...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []), + ` ret void`, + `rcok${i}:`, + ); + if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`); passed.push(`ptr %rc${i}`); - } else if (isEncPos) { - // Node's encoding for decoded (Buffer) chunks is 'buffer'. - host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`); - d.push(` %en${i} = call ptr @scr_str_new(ptr ${host.cstr("buffer")}, ${host.sizeType} 6)`); + } else if (isChunkPos) { + // Any other declared type converts from the chunk's value (an + // object-mode side's value, or the Buffer), or throws out of the + // write. + const helper = host.dyn.dynCheckHelper(p, false); + const ty = host.llType(p); + host.declare(`declare ptr @scr_stream_chunk_value(ptr, ptr)`); + host.declare(`declare void @scr_dyn_release(ptr)`); + host.declare(`declare zeroext i1 @scr_exc_pending()`); + if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`); + d.push( + ` %cv${i} = call ptr @scr_stream_chunk_value(ptr %s, ptr %chunk)`, + ` %ct${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %cv${i}, ptr null)`, + ` call void @scr_dyn_release(ptr %cv${i})`, + ` %ctbad${i} = call zeroext i1 @scr_exc_pending()`, + ` br i1 %ctbad${i}, label %ctfail${i}, label %ctok${i}`, + `ctfail${i}:`, + ...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []), + ` ret void`, + `ctok${i}:`, + ); + passed.push(`${ty} %ct${i}`); + } else if (isEncPos && p.kind === "string") { + // Node's encoding for decoded (Buffer) chunks is 'buffer'; an + // object-mode side passes the write's encoding argument ("" for + // undefined and null). + host.declare(`declare ptr @scr_stream_chunk_encoding(ptr)`); + d.push(` %en${i} = call ptr @scr_stream_chunk_encoding(ptr %s)`); passed.push(`ptr %en${i}`); + } else if (isEncPos) { + // Another declared type (string | null | undefined) converts from + // the encoding argument's value. + const helper = host.dyn.dynCheckHelper(p, false); + const ty = host.llType(p); + host.declare(`declare ptr @scr_stream_chunk_encoding_value(ptr)`); + host.declare(`declare void @scr_dyn_release(ptr)`); + host.declare(`declare zeroext i1 @scr_exc_pending()`); + if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`); + d.push( + ` %ev${i} = call ptr @scr_stream_chunk_encoding_value(ptr %s)`, + ` %et${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %ev${i}, ptr null)`, + ` call void @scr_dyn_release(ptr %ev${i})`, + ` %etbad${i} = call zeroext i1 @scr_exc_pending()`, + ` br i1 %etbad${i}, label %etfail${i}, label %etok${i}`, + `etfail${i}:`, + ...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []), + ` ret void`, + `etok${i}:`, + ); + passed.push(`${ty} %et${i}`); } else if (isErrPos) { // destroy's error argument: `Error | null` — wrap type-directedly. // The finished/pipeline callback ("e") may declare `Error | null | diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 699e52575a..3e8a9bff53 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -924,32 +924,41 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr } if (e.fn === "emitter.emitData") { // A user emit('data', chunk) on a stream-rooted receiver: fill the - // matching payload slot of the two-slot 'data' ABI, NULL the other. + // matching payload slot of the three-slot 'data' ABI (bytes, string, + // object-mode value), NULL the others; a checked-dynamic chunk fills + // them by tag (scr_stream_emit_value). const args = e.args.map((a) => host.emitExpr(a)); const chunkT = e.args[2]!.type; - const both = - chunkT.kind === "string" - ? [`ptr null`, `ptr ${args[2]!.name}`] - : [`ptr ${args[2]!.name}`, `ptr null`]; - host.declare(`declare zeroext i1 @scr_emitter_emit(ptr, ptr, ...)`); const t = B.tmp(); - B.line( - `${t} = call zeroext i1 (ptr, ptr, ...) @scr_emitter_emit(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ${both.join(", ")})`, - ); + if (chunkT.kind === "dyn") { + host.declare(`declare zeroext i1 @scr_stream_emit_value(ptr, ptr, ptr)`); + B.line( + `${t} = call zeroext i1 @scr_stream_emit_value(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name})`, + ); + } else { + const slots = + chunkT.kind === "string" + ? [`ptr null`, `ptr ${args[2]!.name}`, `ptr null`] + : [`ptr ${args[2]!.name}`, `ptr null`, `ptr null`]; + host.declare(`declare zeroext i1 @scr_emitter_emit(ptr, ptr, ...)`); + B.line( + `${t} = call zeroext i1 (ptr, ptr, ...) @scr_emitter_emit(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ${slots.join(", ")})`, + ); + } host.emitPendingCheck(); return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type }; } if (e.fn === "emitter.onData" || e.fn === "emitter.onDataDyn") { // The stream-'data' registration: same runtime entries as - // emitter.on/onDyn, but the DATA adapter (the two-slot payload ABI - // — scr_stream_emit_data) behind the arity-2 fixed shim. + // emitter.on/onDyn, but the DATA adapter (the three-slot payload ABI + // — scr_stream_emit_data) behind the arity-3 fixed shim. const isDyn = e.fn === "emitter.onDataDyn"; const cbT = e.args[isDyn ? 3 : 2]!.type; if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} listener not a func`); const args = e.args.map((a) => host.emitExpr(a)); const adapterFn = host.streamDataAdapter(cbT); - host.declare(`declare void @scr_ee_inv_fixed2(ptr, ptr)`); + host.declare(`declare void @scr_ee_inv_fixed3(ptr, ptr)`); const t = B.tmp(); if (isDyn) { host.toHeap(args[3]!); @@ -959,7 +968,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr ); B.line( `${t} = call ptr @scr_emitter_on_dyn(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` + - `ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed2, i1 ${args[4]!.name}, i1 ${args[5]!.name})`, + `ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed3, i1 ${args[4]!.name}, i1 ${args[5]!.name})`, ); } else { host.declare(`declare ptr @scr_closure_retain_v(ptr)`); @@ -972,7 +981,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr ); B.line( `${t} = call ptr @scr_emitter_on_via(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` + - `ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed2, i1 ${args[3]!.name}, i1 ${args[4]!.name})`, + `ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed3, i1 ${args[3]!.name}, i1 ${args[4]!.name})`, ); } const out = host.own({ name: t, type: e.type }); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index c08d7b720f..032d44d50a 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1113,10 +1113,13 @@ export const LIB_FN_SYMS: Record = { "readable.pushStr": "scr_stream_push_str", "readable.pushStrEnc": "scr_stream_push_str_enc", "readable.pushEncoding": "scr_stream_set_push_encoding", + "stream.objectMode": "scr_stream_object_mode", + "stream.chunkDone": "scr_stream_chunk_done", "readable.pushNull": "scr_stream_push_null", "readable.pushDyn": "scr_stream_push_dyn", "readable.unshift": "scr_stream_unshift", "readable.unshiftStr": "scr_stream_unshift_str", + "readable.unshiftDyn": "scr_stream_unshift_dyn", "readable.pause": "scr_stream_pause", "readable.resume": "scr_stream_resume", "readable.isPaused": "scr_stream_is_paused", @@ -1667,6 +1670,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "readable.pushDyn", "readable.unshift", "readable.unshiftStr", + "readable.unshiftDyn", "readable.read", "readable.readDyn", "readable.setEncoding", @@ -1698,6 +1702,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "console.write", "writable.writeU", "writable.writeDyn", + "writable.writeEnc", "writable.end", "writable.uncork", "stream.destroy", diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index 5d198d306c..6d6c6f99c1 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -419,10 +419,34 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV host.emitPendingCheck(); return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type }; } + if (e.fn === "writable.writeEnc") { + // (recv, flags, chunk[, encoding][, cb]) — flags: 1 encoding, 2 + // callback (MOVES). Without an encoding the argument is undefined, or + // null when the callback took its place. + const flagsArg = e.args[1]!; + if (flagsArg.kind !== "numLit") + throw new InternalCompilerError("llvm emitter bug: writable.writeEnc flags not a literal"); + const flags = flagsArg.value; + const args = e.args.map((a) => host.emitExpr(a)); + let at = 3; + const enc = flags & 1 ? args[at++]!.name : writeEncNull(host, flags & 2 ? "null" : "undef"); + let cb = "null"; + if (flags & 2) { + host.toHeap(args[at]!); + cb = args[at]!.name; + } + host.declare(`declare zeroext i1 @scr_stream_write_enc(ptr, ptr, ptr, ptr)`); + const t = B.tmp(); + B.line( + `${t} = call zeroext i1 @scr_stream_write_enc(ptr ${args[0]!.name}, ptr ${args[2]!.name}, ptr ${enc}, ptr ${cb})`, + ); + host.emitPendingCheck(); + return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type }; + } if (e.fn === "writable.end") { - // (recv, flags[, chunk][, cb]) — flags: 1 bytes chunk, 2 string - // chunk, 8 dyn chunk (write first, Node's end(chunk) decomposition), - // 4 callback. + // (recv, flags[, chunk][, encoding][, cb]) — flags: 1 bytes chunk, 2 + // string chunk, 8 dyn chunk (write first, Node's end(chunk) + // decomposition), 16 an encoding after the dyn chunk, 4 callback. const flagsArg = e.args[1]!; if (flagsArg.kind !== "numLit") throw new InternalCompilerError("llvm emitter bug: writable.end flags not a literal"); @@ -432,19 +456,27 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV let chunkB = "null"; let chunkS = "null"; let chunkD: string | null = null; + let encD = "null"; if (flags & 1) chunkB = args[at++]!.name; else if (flags & 2) chunkS = args[at++]!.name; else if (flags & 8) chunkD = args[at++]!.name; + if (flags & 16) encD = args[at++]!.name; + else if (chunkD !== null && flags & 4) encD = writeEncNull(host, "null"); let cbName = "null"; if (flags & 4) { host.toHeap(args[at]!); cbName = args[at]!.name; } if (chunkD !== null) { - host.declare(`declare zeroext i1 @scr_stream_write_dyn(ptr, ptr, ptr)`); + // end(chunk, cb) writes with a null encoding argument (Node's end + // shifts the callback out of it) + host.declare(`declare zeroext i1 @scr_stream_write_enc(ptr, ptr, ptr, ptr)`); B.line( - `${B.tmp()} = call zeroext i1 @scr_stream_write_dyn(ptr ${args[0]!.name}, ptr ${chunkD}, ptr null)`, + `${B.tmp()} = call zeroext i1 @scr_stream_write_enc(ptr ${args[0]!.name}, ptr ${chunkD}, ptr ${encD}, ptr null)`, ); + // a write that throws (an unknown encoding) leaves the stream as it + // was: Node's end() throws before it ends the stream + host.emitPendingCheck(); } host.declare(`declare ptr @scr_stream_end(ptr, ptr, ptr, ptr)`); const t = B.tmp(); @@ -771,3 +803,13 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV // adapters; the handful of union-wrapped reads use the envGet shapes. return host.emitGenericLibCall(e); } + +/** The encoding argument a write passes when the call has none: undefined + * (a null pointer), or the null value when a callback took its place. */ +function writeEncNull(host: LlvmEmitterContext, which: "null" | "undef"): string { + if (which === "undef") return "null"; + host.declare(`declare ptr @scr_dyn_new_null()`); + const t = host.B.tmp(); + host.B.line(`${t} = call ptr @scr_dyn_new_null()`); + return t; +} diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index f025103389..703e8fe48f 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 = 25 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v25" as const; +export const RUNTIME_ABI_VERSION = 26 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v26" 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 8bdbc84920..a49c36ac3a 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: 25, marker: "scr_runtime_abi_v25" }, + runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" }, 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 a3ac9fe5d3..53afafe16b 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -124,7 +124,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, + runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" }, 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 0e48f20400..5b427a750f 100644 --- a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts +++ b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts @@ -428,7 +428,9 @@ function lowerNodeReadable( }, { kind: "if", - cond: { kind: "dynTest", test: "undefined", value: chunkRef, type: BOOL, loc }, + // the end is the done marker: an object-mode stream can yield + // undefined + cond: { kind: "libCall", fn: "stream.chunkDone", args: [chunkRef], type: BOOL, loc }, then: [{ kind: "break", loc }], else_: null, loc, diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index f105af786d..f44a3d6d8d 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -1458,10 +1458,11 @@ export function lowerEmitterMethodCall( const receiver = lowerReceiver(); const once = member === "once" || member === "prependOnceListener"; const prepend = member === "prependListener" || member === "prependOnceListener"; - // Stream 'data' rides its own two-slot payload ABI (bytes + string — - // encoded streams deliver strings; scr_stream_emit_data): listeners - // on readable-sided receivers register through DATA thunks that - // unwrap the declared side (typed) or box by tag (dyn). + // Stream 'data' rides its own three-slot payload ABI (bytes, string — + // encoded streams deliver strings — and an object-mode value; + // scr_stream_emit_data): listeners on readable-sided receivers + // register through DATA thunks that unwrap the declared side, convert + // to the declared type, or take the value (dyn). const sides = streamSidesOf(lowerer, info); if (name === "data" && (sides === "r" || sides === "rw")) { const dynFlavor = dynListenerFlavor(lowerer, args[1]!); @@ -1500,16 +1501,25 @@ export function lowerEmitterMethodCall( (lowerer.unions.get(p.unionId)?.arms ?? []).every( (arm) => (arm.kind === "bytes" && arm.elem === "u8") || arm.kind === "string", ); + // Any other chunk type converts from the chunk's value (an + // object-mode stream's value, or the Buffer or string) when the + // listener runs, or throws a TypeError there. if ( p !== undefined && !(p.kind === "bytes" && p.elem === "u8") && p.kind !== "string" && - !chunkUnion + !chunkUnion && + (p.kind === "func" || + !canConvertToDyn( + p, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + )) ) { lowerer.noLowering( `a 'data' listener whose chunk parameter is '${lowerer.fmt(p)}'`, args[1]!, - "chunks are Buffers — or strings once setEncoding/the encoding option applies", + "declare the chunk as a type that converts from a checked-dynamic value", ); } if (!registering) { @@ -1654,8 +1664,10 @@ export function lowerEmitterMethodCall( loc, ); } - // Stream 'data' rides the two-slot payload ABI: a user emit fills - // the chunk's slot (bytes or string), NULLs the other. + // Stream 'data' rides the three-slot payload ABI: a user emit fills + // the chunk's slot (bytes or string) and NULLs the others; any other + // value is emitted as a checked-dynamic value, filling the slots by + // its tag. const emitSides = streamSidesOf(lowerer, info); if (name === "data" && (emitSides === "r" || emitSides === "rw")) { if (args.length !== 2) { @@ -1665,16 +1677,27 @@ export function lowerEmitterMethodCall( "the event carries one chunk (a Buffer or string)", ); } - const chunk = lowerer.lowerExpr(args[1]!); + let chunk = lowerer.lowerExpr(args[1]!); if ( !(chunk.type.kind === "bytes" && chunk.type.elem === "u8") && chunk.type.kind !== "string" ) { - lowerer.noLowering( - `emit('data') with a '${lowerer.fmt(chunk.type)}' chunk`, - args[1]!, - "chunks are Buffers or strings", - ); + if ( + chunk.type.kind === "func" || + (chunk.type.kind !== "dyn" && + !canConvertToDyn( + chunk.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + )) + ) { + lowerer.noLowering( + `emit('data') with a '${lowerer.fmt(chunk.type)}' chunk`, + args[1]!, + "the chunk is a value that converts to a checked-dynamic value", + ); + } + chunk = lowerer.coerceToExpected(chunk, DYN); } return { kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index d6f89dc11e..14062051fa 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -308,6 +308,21 @@ function lowerStreamCallback( // boxes each position by kind (the completion callback arrives as a // callable dyn whose glue dispatches to the runtime's *_done). if (funcType.params[i]!.kind === "dyn") continue; + // A write or transform chunk of another type converts from the + // chunk's value when the callback runs (an object-mode side's value, + // or the Buffer), or throws a TypeError there; an encoding of another + // type (`string | null | undefined`) from the encoding argument's. + if ( + (i === 0 || i === 1) && + (which === "write" || which === "transform") && + funcType.params[i]!.kind !== "func" && + canConvertToDyn( + funcType.params[i]!, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) + continue; if (!typeEquals(funcType.params[i]!, fullTuple[i]!)) { lowerer.noLowering( `a '${which}' callback whose parameter ${i} is '${lowerer.fmt(funcType.params[i]!)}' where Node passes '${lowerer.fmt(fullTuple[i]!)}'`, @@ -404,11 +419,52 @@ interface StreamOptions { * right after construction (Node's constructors call addAbortSignal * once the state exists), or null when absent. */ signal: IrExpr | null; + /** The objectMode option's sides (a Duplex's readableObjectMode and + * writableObjectMode set one side): applied with stream.objectMode + * right after construction. */ + readableObject: boolean; + writableObject: boolean; /** Present callbacks in canonical order with their flag bits. */ flags: number; cbs: IrExpr[]; } +/** The objectMode, readableObjectMode and writableObjectMode members of a + * stream options literal (boolean literals; the sided keys count only on + * a Duplex, Transform or PassThrough, as in Node). */ +function streamObjectModes( + lowerer: Lowerer, + ctorName: string, + optsLit: ts.ObjectLiteralExpression | undefined, +): { readable: boolean; writable: boolean } { + const out = { readable: false, writable: false }; + const duplexish = ctorName !== "Readable" && ctorName !== "Writable"; + for (const prop of optsLit?.properties?.nodes ?? []) { + if (!ts.isPropertyAssignment(prop)) continue; + const name = + ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : null; + if (name !== "objectMode" && name !== "readableObjectMode" && name !== "writableObjectMode") + continue; + const value = prop.initializer; + if (value.kind === ts.SyntaxKind.FalseKeyword) continue; + if (value.kind !== ts.SyntaxKind.TrueKeyword) { + lowerer.noLowering( + `${ctorName} with a runtime '${name}' value`, + prop, + `write '${name}: true' or '${name}: false'`, + ); + } + if (name === "objectMode") { + out.readable = true; + out.writable = true; + } else if (duplexish) { + if (name === "readableObjectMode") out.readable = true; + else out.writable = true; + } + } + return out; +} + /** The full Node parameter tuples of the option callbacks, given the * concrete stream's `this` type. The trailing completion callbacks keep * whatever func type the checker contextually gave the user's parameter @@ -657,6 +713,8 @@ function parseStreamOptions( badEncoding: null, pushEncoding: null, signal: null, + readableObject: false, + writableObject: false, flags: 0, cbs: [], }; @@ -669,6 +727,18 @@ function parseStreamOptions( "the options must be an object literal, inline or through const-initialized bindings (the callbacks lower as compile-time closures)", ); } + // Read before the callbacks lower: an object-mode readable side keeps a + // completion callback's `data: any` (its values are not only strings and + // Buffers). An object-mode side's high-water mark defaults to 16. + const objectModes = streamObjectModes( + lowerer, + ctorName, + optsLit !== undefined && ts.isObjectLiteralExpression(optsLit) ? optsLit : undefined, + ); + out.readableObject = objectModes.readable; + out.writableObject = objectModes.writable; + if (out.readableObject) out.hwmR = numLit(16, loc); + if (out.writableObject) out.hwmW = numLit(16, loc); const tuples = cbTuples(lowerer); const present = new Map(); for (const prop of optsLit?.properties?.nodes ?? []) { @@ -715,7 +785,7 @@ function parseStreamOptions( continue; } const full = tuples[name]!.map((t) => (t === "fn" ? null : t)); - const pinned = pinAnyDataCompletion(lowerer, name, fnNode); + const pinned = out.readableObject ? null : pinAnyDataCompletion(lowerer, name, fnNode); try { // The completion-callback tail: typed by the checker contextually; // accept the user's declared func type as the tuple's entry so the @@ -776,16 +846,9 @@ function parseStreamOptions( break; case "objectMode": case "readableObjectMode": - case "writableObjectMode": { - // `objectMode: false` is the byte default — admit the literal. - if (value.kind === ts.SyntaxKind.FalseKeyword) break; - lowerer.noLowering( - `${ctorName} in objectMode`, - prop, - "object-mode streams have no static chunk type yet — chunks are Buffers (utf8 strings convert at push/write)", - ); + case "writableObjectMode": + // read up front (streamObjectModes) break; - } case "decodeStrings": { if (value.kind === ts.SyntaxKind.TrueKeyword) break; // Node's default lowerer.noLowering( @@ -942,6 +1005,8 @@ function streamCtorArgs( badEncoding: string | null; pushEncoding: string | null; signal: IrExpr | null; + readableObject: boolean; + writableObject: boolean; } { const shape = streamCtorShape(cls); const o = parseStreamOptions( @@ -971,6 +1036,8 @@ function streamCtorArgs( badEncoding: o.badEncoding, pushEncoding: o.pushEncoding, signal: o.signal, + readableObject: o.readableObject, + writableObject: o.writableObject, }; } @@ -1015,6 +1082,17 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C ); } let built: IrExpr = { kind: "libCall", fn, args: parsed.args, type: thisType, loc }; + if (parsed.readableObject || parsed.writableObject) { + // { objectMode }: applied before any other follow-up (answers the + // receiver +1). + built = { + kind: "libCall", + fn: "stream.objectMode", + args: [built, boolLit(parsed.readableObject, loc), boolLit(parsed.writableObject, loc)], + type: thisType, + loc, + }; + } if (parsed.pushEncoding !== null) { // { defaultEncoding }: chain the push-side decode default (the // setEncoding chaining shape — answers the receiver +1). @@ -1133,6 +1211,22 @@ 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. + if ( + i === 1 && + (which === "write" || which === "transform") && + got.type.kind !== "func" && + canConvertToDyn( + got.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + paramTypes.push(got.type); + continue; + } // A declared supertype of what Node passes (`chunk: Buffer | string`) // is sound: the wrapper takes Node's value and widens it into the // declared parameter. @@ -1140,6 +1234,22 @@ function streamMethodWrapper( paramTypes.push(want); continue; } + // A write or transform chunk of another type converts from the + // chunk's value when the method runs (an object-mode side's value, + // or the Buffer), or throws a TypeError there. + if ( + i === 0 && + (which === "write" || which === "transform") && + got.type.kind !== "func" && + canConvertToDyn( + got.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + paramTypes.push(got.type); + continue; + } lowerer.noLowering( `a '${methodName}' method whose parameter ${i} is '${lowerer.fmt(got.type)}' where Node passes '${lowerer.fmt(want)}'`, blame, @@ -1301,6 +1411,19 @@ export function lowerStreamSuperCall( loc, }, ]; + if (parsed.readableObject || parsed.writableObject) { + out.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "stream.objectMode", + args: [thisRef(), boolLit(parsed.readableObject, loc), boolLit(parsed.writableObject, loc)], + type: thisType, + loc, + }, + loc, + }); + } if (parsed.pushEncoding !== null) { out.push({ kind: "exprStmt", @@ -1602,7 +1725,17 @@ export function lowerStreamStaticCall( } const anySrc = srcT?.kind === "dyn" || - (srcT?.kind === "set" && typedElem(srcT.elem) !== null) || + // a Set or Map (entry arrays) of other values: the checked-dynamic + // view iterates the collection itself + ((srcT?.kind === "set" || srcT?.kind === "map") && + canConvertToDyn( + srcT, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + )) || + // an array of other values: the object-mode entries come from its + // checked-dynamic copy + (srcT?.kind === "array" && fromGenChunk(lowerer, srcT.elem) && typedElem(srcT.elem) === null) || (srcNode !== undefined && srcT === null && (lowerer.typeOf(srcNode).flags & ts.TypeFlags.Any) !== 0); @@ -1623,20 +1756,24 @@ export function lowerStreamStaticCall( lowerer.noLowering( `Readable.from over a '${srcT ? lowerer.fmt(srcT) : "?"}' source`, srcNode ?? call, - "the supported sources are a string or Buffer, and arrays, Sets, generators and class iterables of strings or Buffers", + "the supported sources are a string or Buffer, and arrays, Sets, Maps, generators and class iterables of values that convert to checked-dynamic values", ); } -/** Whether a generator's yields are stream chunks: strings, Buffers, a - * union of them (null included: the runtime destroys the stream with - * ERR_STREAM_NULL_VALUES), or checked-dynamic values (checked when - * pushed). */ +/** Whether a generator's yields can be stream chunks: values that box + * into checked-dynamic values (null included: the runtime destroys the + * stream with ERR_STREAM_NULL_VALUES). An object-mode stream takes any of + * them; a byte-mode one checks them when they are pushed or written. */ function fromGenChunk(lowerer: Lowerer, t: IrType): boolean { - const chunk = (a: IrType): boolean => - a.kind === "string" || (a.kind === "bytes" && a.elem === "u8") || a.kind === "nullT"; - if (t.kind === "dyn" || (chunk(t) && t.kind !== "nullT")) return true; - if (t.kind !== "union") return false; - return lowerer.unions.get(t.unionId)?.arms.every(chunk) ?? false; + if (t.kind === "nullT" || t.kind === "void" || t.kind === "func") return false; + return ( + t.kind === "dyn" || + canConvertToDyn( + t, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ); } /** The helpers Readable.from pulls a compiled generator through, as @@ -3587,14 +3724,6 @@ function lowerStreamOperator( throw new InternalCompilerError(`unknown stream operator ${member}`); } -/** The literal 'buffer' encoding on write()/end(): Node 24 takes it for a - * Buffer chunk and throws ERR_UNKNOWN_ENCODING for a string chunk. */ -function isBufferEncoding(lowerer: Lowerer, arg: ts.Expression | undefined): boolean { - if (arg === undefined) return false; - const t = lowerer.typeOf(arg); - return t.isStringLiteralType() && t.value === "buffer"; -} - /** The one supported encoding argument: utf8 (Node's default), as a * compile-time literal or absent. */ function checkUtf8Encoding(lowerer: Lowerer, member: string, arg: ts.Expression | undefined): void { @@ -3925,10 +4054,24 @@ export function lowerStreamMethodCall( if (probed?.type.kind === "dyn") return "dyn"; if (probed?.type.kind === "nullT") return "null"; } + // Any other value is a checked-dynamic chunk: one entry on an + // object-mode side, Node's chunk type error on a byte-mode one + // (undefined, which a byte-mode push ignores, included). + if (t?.kind === "void" || t?.kind === "undefinedT") return "dyn"; + if ( + t !== null && + t.kind !== "func" && + canConvertToDyn( + t, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) + return "dyn"; lowerer.noLowering( `${member} with a '${t ? lowerer.fmt(t) : "?"}' chunk`, node, - "chunks are Buffers or utf8 strings — or unions of those with null (objectMode is not lowered)", + "chunks are values that convert to checked-dynamic values: Buffers, strings, numbers, records, arrays and class instances", ); }; @@ -3973,11 +4116,17 @@ export function lowerStreamMethodCall( checkUtf8Encoding(lowerer, member, args[1]); const kind = chunkKind(args[0]!); if ((kind === "null" || kind === "union" || kind === "dyn") && member === "unshift") { - lowerer.noLowering( - kind === "null" ? "unshift(null)" : "unshift with a nullable or dynamic chunk", - call, - "EOF signals through push(null)", - ); + // 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 chunk = lowerer.lowerExprExpecting(args[0]!, DYN); + return { + kind: "libCall", + fn: "readable.unshiftDyn", + args: [receiver, chunk], + type: VOID, + loc, + }; } const receiver = lowerer.lowerExpr(access.expression); if (kind === "null") { @@ -4237,9 +4386,54 @@ export function lowerStreamMethodCall( } const kind = chunkKind(args[0]!); if (kind === "null") lowerer.noLowering("write(null)", call, "end() finishes a writable"); + const lowerWriteCb = (cbArg: ts.Expression): IrExpr => { + const cb = callbackDeclaresParameters(cbArg) + ? null + : lowerer.lowerExprExpecting(cbArg, funcOf([], VOID)); + if (cb === null || cb.type.kind !== "func" || cb.type.params.length > 0) { + lowerer.noLowering( + "write completion callbacks taking the error argument", + cbArg, + "() => void callbacks are supported (listen for 'error' to observe failures)", + ); + } + return cb!; + }; + // write(chunk, encoding[, cb]), and write(value, cb): the encoding + // argument reaches an object-mode side's _write as passed, null when + // the callback took its place (a byte-mode side encodes a string + // chunk with it). + const writeEnc = (encArg: ts.Expression | undefined, cbArg: ts.Expression | undefined) => { + const receiver = lowerer.lowerExpr(access.expression); + const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); + const tail: IrExpr[] = []; + if (encArg !== undefined) tail.push(lowerer.lowerExprExpecting(encArg, DYN)); + if (cbArg !== undefined) tail.push(lowerWriteCb(cbArg)); + const flags = (encArg !== undefined ? 1 : 0) | (cbArg !== undefined ? 2 : 0); + return { + kind: "libCall", + fn: "writable.writeEnc", + args: [receiver, numLit(flags, loc), chunk, ...tail], + type: BOOL, + loc, + } satisfies IrExpr; + }; if (kind === "dyn") { - if (args.length > 1) - lowerer.noLowering("write with a dynamic chunk and more arguments", call); + if (args.length === 3) return writeEnc(args[1], args[2]); + if (args.length === 2) { + // Node's write takes a function there as the callback, anything + // else as the encoding + const t = lowerer.mapTypeOf(lowerer.typeOf(args[1]!)); + if (t?.kind === "func") return writeEnc(undefined, args[1]); + if (t === null || t === undefined || t.kind === "dyn") { + lowerer.noLowering( + "write with a dynamic second argument", + args[1]!, + "pass an encoding string or a completion callback", + ); + } + return writeEnc(args[1], undefined); + } const receiver = lowerer.lowerExpr(access.expression); const chunk = lowerer.lowerExprExpecting(args[0]!, DYN); return { kind: "libCall", fn: "writable.writeDyn", args: [receiver, chunk], type: BOOL, loc }; @@ -4263,19 +4457,15 @@ export function lowerStreamMethodCall( return { kind: "libCall", fn, args: [receiver, chunk], type: BOOL, loc }; } // Disambiguate (chunk, cb?) vs (chunk, encoding, cb?) by the second - // argument's static type. + // argument's static type. A write with an encoding runs through + // writeEnc, whose runtime checks and applies it as Node's _write does. let cbArg: ts.Expression | undefined; - if (args.length >= 2 && isBufferEncoding(lowerer, args[1])) { - if (kind !== "bytes") - return nodeThrowExpr(1, "ERR_UNKNOWN_ENCODING", "Unknown encoding: buffer", BOOL, loc); - cbArg = args[2]; - } else if (args.length === 2) { + if (args.length === 3) return writeEnc(args[1], args[2]); + if (args.length === 2) { const t = lowerer.mapTypeOf(lowerer.typeOf(args[1]!)); - if (t?.kind === "string") checkUtf8Encoding(lowerer, "write", args[1]); - else cbArg = args[1]; - } else if (args.length === 3) { - checkUtf8Encoding(lowerer, "write", args[1]); - cbArg = args[2]; + if (t?.kind === "string" || t?.kind === "nullT" || t?.kind === "undefinedT") + return writeEnc(args[1], undefined); + cbArg = args[1]; } const receiver = lowerer.lowerExpr(access.expression); const chunk = @@ -4284,16 +4474,7 @@ export function lowerStreamMethodCall( : lowerer.lowerExprExpecting(args[0]!, STRING); const fn = kind === "bytes" ? "writable.write" : "writable.writeStr"; if (cbArg !== undefined) { - const cb = callbackDeclaresParameters(cbArg) - ? null - : lowerer.lowerExprExpecting(cbArg, funcOf([], VOID)); - if (cb === null || cb.type.kind !== "func" || cb.type.params.length > 0) { - lowerer.noLowering( - "write completion callbacks taking the error argument", - cbArg, - "() => void callbacks are supported (listen for 'error' to observe failures)", - ); - } + const cb = lowerWriteCb(cbArg); return { kind: "libCall", fn, args: [receiver, chunk, cb], type: BOOL, loc }; } return { kind: "libCall", fn, args: [receiver, chunk], type: BOOL, loc }; @@ -4305,34 +4486,29 @@ export function lowerStreamMethodCall( // Forms: (), (cb), (chunk), (chunk, cb), (chunk, encoding, cb). let chunkNode: ts.Expression | undefined; let cbArg: ts.Expression | undefined; + let encNode: ts.Expression | undefined; if (args.length === 1) { const t = lowerer.mapTypeOf(lowerer.typeOf(args[0]!)); if (t?.kind === "func") cbArg = args[0]; else chunkNode = args[0]; - } else if (args.length >= 2 && isBufferEncoding(lowerer, args[1])) { - chunkNode = args[0]; - const endKind = chunkKind(args[0]!); - if (endKind === "string") { - const endT = lowerer.mapTypeOf(lowerer.typeOf(call)) ?? DYN; - return nodeThrowExpr(1, "ERR_UNKNOWN_ENCODING", "Unknown encoding: buffer", endT, loc); - } - if (endKind !== "bytes") checkUtf8Encoding(lowerer, "end", args[1]); - cbArg = args[2]; } else if (args.length === 2) { chunkNode = args[0]; const t = lowerer.mapTypeOf(lowerer.typeOf(args[1]!)); - if (t?.kind === "string") checkUtf8Encoding(lowerer, "end", args[1]); + if (t?.kind === "string" || t?.kind === "nullT" || t?.kind === "undefinedT") + encNode = args[1]; else cbArg = args[1]; } else if (args.length === 3) { chunkNode = args[0]; - checkUtf8Encoding(lowerer, "end", args[1]); + encNode = args[1]; cbArg = args[2]; } const receiver = lowerer.lowerExpr(access.expression); let flags = 0; const tail: IrExpr[] = []; if (chunkNode !== undefined) { - const kind = chunkKind(chunkNode); + // end(chunk, encoding[, cb]) writes the chunk with the encoding + // argument as passed (flags 16), through the dyn write + const kind = encNode !== undefined ? "dyn" : chunkKind(chunkNode); if (kind === "null" || kind === "union") lowerer.noLowering("end(null)", call); flags |= kind === "bytes" ? 1 : kind === "dyn" ? 8 : 2; tail.push( @@ -4342,6 +4518,10 @@ export function lowerStreamMethodCall( ? lowerer.lowerExprExpecting(chunkNode, DYN) : lowerer.lowerExprExpecting(chunkNode, STRING), ); + if (encNode !== undefined) { + flags |= 16; + tail.push(lowerer.lowerExprExpecting(encNode, DYN)); + } } if (cbArg !== undefined) { const cb = callbackDeclaresParameters(cbArg) @@ -4604,8 +4784,7 @@ export function lowerStreamProperty( return propCall(BOOL); } if (name === "writableObjectMode" && writableSide) { - // objectMode construction fences, so the answer is compile-time false. - return boolLit(false, loc); + return propCall(BOOL); } if (name === "allowHalfOpen" && sides === "rw") { return propCall(BOOL); diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index dd82d06c2d..8a4e1848da 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -358,6 +358,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "readable.pushU", "readable.pushDyn", "readable.unshift", + "readable.unshiftDyn", "readable.unshiftStr", "readable.read", "readable.readDyn", @@ -370,6 +371,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "writable.writeStr", "writable.writeU", "writable.writeDyn", + "writable.writeEnc", "writable.end", "writable.uncork", "stream.destroy", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 8f5928d12d..8104c73923 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3875,7 +3875,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * readable.unpipe (recv[, dst]) → recv +1. * writable.write/writeStr: (recv, chunk[, cb]) → below-hwm bool (cb * MOVES when present, called after the user write completes). - * writable.end: (recv, flags[, chunk][, cb]) → recv +1. cork/uncork are + * writable.writeEnc: (recv, flags, chunk[, encoding][, cb]) — flags 1 + * an encoding argument, 2 a callback (the encoding is then null, as + * Node's write shifts it). + * writable.end: (recv, flags[, chunk][, encoding][, cb]) → recv +1; + * flags 16: an encoding argument follows the dyn chunk. cork/uncork are * void. stream.destroy/destroyErr: (recv[, err]) → recv +1. * stream.prop: (recv, name-literal) → the flag/number the name asks * for; stream.errored → Error|null union; readable.flowing → @@ -4014,6 +4018,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "readable.pushU": { argTypes: [null, null], result: BOOL }, "readable.pushDyn": { argTypes: [null, DYN], result: BOOL }, "readable.unshift": { argTypes: [null, BYTES_U8], result: VOID }, + "readable.unshiftDyn": { argTypes: [null, DYN], result: VOID }, "readable.unshiftStr": { argTypes: [null, STRING], result: VOID }, "stream.onDyn": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID }, "readable.readDyn": { argTypes: [null, DYN], result: DYN }, @@ -4025,6 +4030,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * Buffer.from(chunk, enc)); both carry the CANONICAL literal. */ "readable.pushStrEnc": { argTypes: [null, STRING, STRING], result: BOOL }, "readable.pushEncoding": { argTypes: [null, STRING], result: VOID }, + /** 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 }, + /** Whether a readable.nextChunkDyn step answered the end (the done + * marker; an object-mode stream can yield undefined). */ + "stream.chunkDone": { argTypes: [DYN], result: BOOL }, /** for-await over a readable (the desugared loop's per-pass promise): * +1 promise of the next chunk — buffered content, the EOF sentinel * (empty Buffer / dyn undefined), or a rejection with the stream's @@ -4078,6 +4089,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "writable.writeStr": { argTypes: [null, STRING], result: BOOL }, "writable.writeU": { argTypes: [null, null], result: BOOL }, "writable.writeDyn": { argTypes: [null, DYN], result: BOOL }, + "writable.writeEnc": { argTypes: [null, F64, DYN], result: BOOL }, "writable.end": { argTypes: [null, F64], result: VOID }, "writable.cork": { argTypes: [null], result: VOID }, "writable.uncork": { argTypes: [null], result: VOID }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 9531d78456..1528242c40 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -216,6 +216,8 @@ const ABORT_SIGNAL_LIB_FNS: ReadonlySet = new Set([ /** Entry points that link the stream unit (with STREAM_LIB_PREFIXES). */ const STREAM_LIB_FNS: ReadonlySet = new Set([ "child.stdioStream", + "stream.chunkDone", + "stream.objectMode", "console.bind", "crypto.hashStream", "crypto.hashStreamPiped", diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 9de1b5b622..bb36c70074 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4109,6 +4109,7 @@ function validateFunction( e.fn === "writable.write" || e.fn === "writable.writeStr" || e.fn === "writable.writeU" || + e.fn === "writable.writeEnc" || e.fn === "writable.end" || e.fn === "readable.unpipe" || e.fn === "rl.create" || @@ -5492,6 +5493,7 @@ function validateFunction( e.fn.startsWith("passthrough.") || e.fn === "stream.destroy" || e.fn === "stream.destroyErr" || + e.fn === "stream.objectMode" || e.fn === "stream.iteratorClose" || e.fn === "stream.prop" || e.fn === "stream.errored" || @@ -5782,6 +5784,7 @@ function validateFunction( e.fn === "writable.end" || e.fn === "readable.setEncoding" || e.fn === "readable.pushEncoding" || + e.fn === "stream.objectMode" || e.fn === "stream.destroy" || e.fn === "stream.destroyErr" || e.fn === "stream.iteratorClose" @@ -5898,8 +5901,10 @@ function validateFunction( const chunkT = e.args[2]?.type; const ok = chunkT !== undefined && - ((chunkT.kind === "bytes" && chunkT.elem === "u8") || chunkT.kind === "string"); - if (!ok) err(`libCall emitter.emitData chunk must be bytes or string`, e.loc); + ((chunkT.kind === "bytes" && chunkT.elem === "u8") || + chunkT.kind === "string" || + chunkT.kind === "dyn"); + if (!ok) err(`libCall emitter.emitData chunk must be bytes, string or dyn`, e.loc); if (e.type.kind !== "bool") err(`libCall emitter.emitData must be bool`, e.loc); return true; } diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index c05e7f9949..af8d9b7245 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_v25"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v26"); 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_v25"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v26"); }); 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/7240-objectmode-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7240-objectmode-streams.ts.json new file mode 100644 index 0000000000..0b212fb072 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7240-objectmode-streams.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7240-objectmode-streams.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7241-objectmode-streams.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7241-objectmode-streams.mjs.json new file mode 100644 index 0000000000..22dc5abc4e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7241-objectmode-streams.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7241-objectmode-streams.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7242-objectmode-dynamic.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7242-objectmode-dynamic.cjs.json new file mode 100644 index 0000000000..1b932b82ba --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7242-objectmode-dynamic.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7242-objectmode-dynamic.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7243-for-await-close-order.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7243-for-await-close-order.mjs.json new file mode 100644 index 0000000000..ec479cf59e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7243-for-await-close-order.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7243-for-await-close-order.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7244-objectmode-subclasses.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7244-objectmode-subclasses.ts.json new file mode 100644 index 0000000000..827bba4c09 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7244-objectmode-subclasses.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7244-objectmode-subclasses.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7245-writable-callback-order.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7245-writable-callback-order.mjs.json new file mode 100644 index 0000000000..9485562b30 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7245-writable-callback-order.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7245-writable-callback-order.mjs" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 6cc32d7fa0..edfaa75194 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -338,7 +338,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, + runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index a2c987b972..542e7d48eb 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -435,7 +435,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, + runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index c8fa40b38d..24cd8093b4 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_v25(void) {} +void scr_runtime_abi_v26(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 5e52785880..791c600c7c 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_v25(void); +void scr_runtime_abi_v26(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 209c8c39a9..cdd8156422 100644 --- a/packages/runtime/src/scr_dyn_builtin_stream.c +++ b/packages/runtime/src/scr_dyn_builtin_stream.c @@ -34,8 +34,7 @@ * receiver without a reference; the hook closures outlive the record. * * Not supported here (each throws the "not supported yet" Error where it - * runs): objectMode and the other options scr_stream.c's dyn option walk - * refuses, `_writev` and `_construct`, symbol event names, listeners() / + * runs): the options scr_stream.c's dyn option walk refuses, `_writev` and `_construct`, symbol event names, listeners() / * rawListeners(), `wrap()`, and pipes into values that are neither runtime * streams nor constructor-function streams. */ #include "scr_runtime.h" @@ -275,11 +274,10 @@ static ScrDyn *bcs_done_call(ScrClosure *closure, ScrDyn *const *args, size_t ar break; } default: { + /* the data is pushed as push(data) pushes it */ ScrDyn *data = bcs_arg(args, argc, 1); - ScrBytes *bytes = data->kind == SCR_DYN_BYTES ? scr_bytes_raw_view(data->v.bytes) : NULL; - ScrStr *text = data->kind == SCR_DYN_STR ? scr_str_retain(data->v.str) : NULL; - if (which == BCS_DONE_TRANSFORM) scr_stream_transform_done(s, err, bytes, text); - else scr_stream_flush_done(s, err, bytes, text); + if (which == BCS_DONE_TRANSFORM) scr_stream_transform_done_value(s, err, data); + else scr_stream_flush_done_value(s, err, data); break; } } @@ -362,13 +360,14 @@ static void bcs_read_inv(ScrClosure *cb, ScrStream *s, double size) { scr_dyn_release(object); } -/* _write(chunk, 'buffer', cb) and _transform(chunk, 'buffer', cb): a - * throw propagates to the write() caller, as in Node. */ +/* _write(chunk, 'buffer', cb) and _transform(chunk, 'buffer', cb), or the + * value and the write's encoding argument on an object-mode writable + * side: a throw propagates to the write() caller, as in Node. */ static void bcs_chunk_call(ScrClosure *cb, ScrStream *s, ScrBytes *chunk, const char *method, int done) { ScrDyn *object = scr_box_get_ref(cb->caps[0]); ScrDyn *args[3]; - args[0] = scr_dyn_new_buffer(chunk); - args[1] = bcs_str("buffer"); + args[0] = scr_stream_chunk_value(s, chunk); + args[1] = scr_stream_chunk_encoding_value(s); args[2] = bcs_done(s, done); ScrDyn *result = bcs_invoke(object, method, args, 3); for (int i = 0; i < 3; i++) scr_dyn_release(args[i]); @@ -488,8 +487,8 @@ static void bcs_listener0(ScrClosure *adapter) { bcs_listener_call(adapter, NULL, 0); } -static void bcs_listener_data(ScrClosure *adapter, ScrBytes *bytes, ScrStr *text) { - ScrDyn *arg = text ? scr_dyn_new_str(text) : scr_dyn_new_buffer(bytes); +static void bcs_listener_data(ScrClosure *adapter, ScrBytes *bytes, ScrStr *text, ScrDyn *value) { + ScrDyn *arg = value ? scr_dyn_retain(value) : text ? scr_dyn_new_str(text) : scr_dyn_new_buffer(bytes); bcs_listener_call(adapter, &arg, 1); scr_dyn_release(arg); } @@ -583,7 +582,7 @@ static bool bcs_add_listener(ScrStream *s, ScrDyn *self, ScrDyn *type, ScrDyn *l break; case 1: chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener_data), - scr_ee_inv_fixed2, once, prepend); + scr_ee_inv_fixed3, once, prepend); break; case 2: chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener_error), @@ -625,14 +624,15 @@ static ScrDyn *bcs_emit(ScrStream *s, ScrDyn *const *args, size_t argc) { had = scr_emitter_emit(em, name); break; case 1: + /* 'data' carries the bytes, string and value slots (scr_stream_emit_data) */ if (first->kind == SCR_DYN_BYTES) { ScrBytes *bytes = scr_bytes_raw_view(first->v.bytes); - had = scr_emitter_emit(em, name, bytes, (ScrStr *)NULL); + had = scr_emitter_emit(em, name, bytes, (ScrStr *)NULL, first); scr_bytes_release(bytes); } else if (first->kind == SCR_DYN_STR) { - had = scr_emitter_emit(em, name, (ScrBytes *)NULL, first->v.str); + had = scr_emitter_emit(em, name, (ScrBytes *)NULL, first->v.str, first); } else { - bcs_unsupported("emit('data') with a chunk other than a Buffer or string on a constructor-function stream is not supported yet"); + had = scr_emitter_emit(em, name, (ScrBytes *)NULL, (ScrStr *)NULL, first); } break; case 2: @@ -1037,10 +1037,16 @@ static ScrDyn *bcs_write(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t ScrDyn *cb = bcs_arg(args, argc, 2); if (scr_dyn_is_callable(encoding)) { cb = encoding; - encoding = scr_dyn_undefined(); + 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)); } - if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) return NULL; - bool below = scr_stream_write_dyn(s, chunk, bcs_callback(cb, self, s)); if (scr_exc_pending()) return NULL; return scr_dyn_new_bool(below); } @@ -1055,11 +1061,17 @@ static ScrDyn *bcs_end(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t a encoding = scr_dyn_undefined(); } else if (scr_dyn_is_callable(encoding)) { cb = encoding; - encoding = scr_dyn_undefined(); + encoding = scr_dyn_new_null(); } - if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) return NULL; ScrStream *chained = NULL; - if (bcs_nullish(chunk)) { + if (!bcs_nullish(chunk) && scr_stream_writes_values(s)) { + /* end(chunk) writes the value with the encoding argument */ + 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)); @@ -1376,8 +1388,6 @@ static ScrDyn *bcs_getter_call(ScrClosure *closure, ScrDyn *const *args, size_t return bcs_flowing(s); case BCS_P_ERRORED: return bcs_errored(s); - case BCS_P_WRITABLE_OBJECT_MODE: - return scr_dyn_new_bool(false); case BCS_P_READABLE_LENGTH: case BCS_P_READABLE_HWM: case BCS_P_WRITABLE_LENGTH: diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 271937f2e2..0c17c8840d 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -80,8 +80,11 @@ static ScrBytes *scr_stream_buffer(ScrBytes *b) { /* ── state ────────────────────────────────────────────────────────────── */ typedef struct ScrWEntry { - ScrBytes *chunk; /* owned */ + ScrBytes *chunk; /* owned; a ScrDyn* value on an object-mode writable side */ ScrClosure *cb; /* owned or NULL (the user's write(chunk, cb) callback) */ + ScrDyn *enc; /* owned or NULL = undefined: the encoding argument an + * object-mode write passed (Node hands it to _write + * unchanged; null when the callback took its place) */ } ScrWEntry; struct ScrStreamState { @@ -132,6 +135,14 @@ struct ScrStreamState { bool object_entries; /* An operator output's entry kind is decided by its first push. */ bool object_kind_set; + /* An object-mode buffer that holds a value other than a string or a + * Buffer, or both kinds: every entry is an owned ScrDyn* + * (scr_stream_add_value). Takes precedence over `encoded`. */ + bool dyn_entries; + /* The objectMode or readableObjectMode option made the buffer hold + * object entries (a stream without _read is not a seeded + * Readable.from source). */ + 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. */ @@ -146,6 +157,11 @@ struct ScrStreamState { * nextTick and then the resumed `await new Promise(next)` * (next_wake: that tick is queued). */ bool next_parked, next_wake; + /* Node's iterator ends through eos, which waits for 'close' when the + * stream will emit it (willEmitClose, fixed when the iterator starts: + * a readable-only stream with autoDestroy and emitClose, not yet + * closed). next_eof_close: the ended waiter is held for 'close'. */ + bool next_started, next_close_wait, next_eof_close; ScrClosure *read_cb; ScrStreamReadInv read_inv; } r; @@ -157,9 +173,23 @@ struct ScrStreamState { size_t n, cap, head; int corked; bool writing, wsync, need_drain, after_scheduled; + /* Deferred afterWrites Node also defers (a user callback, a 'drain' + * or a destroyed stream): its pendingcb, which holds the finish until + * they ran — afterWrite's finishMaybe then puts 'finish' on a tick + * of its own. */ + size_t after_holds; + /* clearBuffer is running its loop: a write completing inside it + * leaves the next buffered write to the loop (Node's + * kBufferProcessing), so completions keep their order */ + bool buffer_processing; + /* objectMode writable side: queued chunks, and the chunk the write + * and transform steps receive, are ScrDyn* values (in the ScrBytes* + * slots), each counting 1 toward length. */ + bool object_mode; bool ending, finished, prefinished, final_called, finish_scheduled; bool final_pending; /* _final called, its callback not yet run */ ScrClosure *inflight_cb; /* owned or NULL */ + ScrDyn *cur_enc; /* borrowed: the running write's encoding argument */ ScrClosure *write_cb; ScrStreamChunkInv write_inv; ScrClosure *final_cb; @@ -267,31 +297,79 @@ static bool scr_stream_is(const ScrEmitter *em) { scr_vt_within(vt, &scr_passthrough_vt); } -/* Readable-buffer entry accessors: bytes chunks, or strings once an +/* Readable-buffer entry accessors: bytes chunks, strings once an * encoding is set (lengths then count JS string units, Node's encoded - * accounting). */ + * accounting), or checked-dynamic values in an object-mode buffer that + * holds other values. */ +typedef enum { SCR_ENTRY_BYTES, SCR_ENTRY_STR, SCR_ENTRY_DYN } ScrEntryKind; + +static ScrEntryKind scr_stream_rkind(const ScrStreamState *st) { + return st->r.dyn_entries ? SCR_ENTRY_DYN : st->r.encoded ? SCR_ENTRY_STR : SCR_ENTRY_BYTES; +} + static size_t scr_stream_entry_len(const ScrStreamState *st, void *e) { if (st->r.object_entries) return 1; /* objectMode-style accounting */ return st->r.encoded ? (size_t)scr_str_utf16_len((ScrStr *)e) : ((ScrBytes *)e)->len; } -static void scr_stream_entry_release(const ScrStreamState *st, void *e) { - if (st->r.encoded) scr_str_release((ScrStr *)e); +/* Releases an entry by the kind it had when it left the buffer: a 'data' + * listener may call setEncoding() or push another kind of value while the + * entry is out. */ +static void scr_stream_entry_release_kind(void *e, ScrEntryKind kind) { + if (kind == SCR_ENTRY_DYN) scr_dyn_release((ScrDyn *)e); + else if (kind == SCR_ENTRY_STR) scr_str_release((ScrStr *)e); else scr_bytes_release((ScrBytes *)e); } -/* Releases an entry by the kind it had when it left the buffer: a 'data' - * listener may call setEncoding() while the entry is out. */ -static void scr_stream_entry_release_kind(void *e, bool is_str) { - if (is_str) scr_str_release((ScrStr *)e); - else scr_bytes_release((ScrBytes *)e); +static void scr_stream_entry_release(const ScrStreamState *st, void *e) { + scr_stream_entry_release_kind(e, scr_stream_rkind(st)); +} + +/* An entry of the given kind as a dyn value (+1; moves entry). */ +static ScrDyn *scr_stream_entry_box(void *entry, ScrEntryKind kind) { + if (kind == SCR_ENTRY_DYN) return entry; + ScrDyn *d; + if (kind == SCR_ENTRY_STR) { + d = scr_dyn_new_str((ScrStr *)entry); + scr_str_release((ScrStr *)entry); + } else { + ScrBytes *b = entry; + d = b->is_buffer ? scr_dyn_new_buffer(b) : scr_dyn_new_bytes(b); + scr_bytes_release(b); + } + return d; +} + +/* Writable-queue chunks: Buffers, or dyn values on an object-mode side. */ +static size_t scr_stream_wchunk_len(const ScrStreamState *st, ScrBytes *chunk) { + return st->w.object_mode ? 1 : chunk->len; +} + +static ScrBytes *scr_stream_wchunk_retain(const ScrStreamState *st, ScrBytes *chunk) { + if (st->w.object_mode) 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); + else scr_bytes_release(chunk); +} + +/* A dyn entry the collector traces, and so frees itself in a collection + * (the kinds that carry a collector header). */ +static bool scr_stream_dyn_entry_traced(const ScrDyn *d) { + return d->kind > SCR_DYN_STR && d->kind != SCR_DYN_UNDEF; } static void scr_stream_state_drop(ScrStreamState *st, bool gc) { if (!st) return; if (st->errored) scr_error_release(st->errored); if (st->native) st->native_release(st->native); - for (size_t i = 0; i < st->r.n; i++) scr_stream_entry_release(st, st->r.buf[i]); + for (size_t i = 0; i < st->r.n; i++) { + /* The collector frees the dyn entries it traced. */ + if (gc && st->r.dyn_entries && scr_stream_dyn_entry_traced(st->r.buf[i])) continue; + scr_stream_entry_release(st, st->r.buf[i]); + } free(st->r.buf); if (st->r.enc) scr_str_release(st->r.enc); if (st->r.push_enc) scr_str_release(st->r.push_enc); @@ -312,8 +390,11 @@ 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++) { - scr_bytes_release(st->w.q[i].chunk); + bool traced = st->w.object_mode && 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); free(st->w.end_cbs); @@ -335,6 +416,11 @@ void scr_stream_st_gcfree(ScrStreamState *st) { scr_stream_state_drop(st, true); void scr_stream_st_trace(ScrStreamState *st, ScrTraceVisit visit, void *ctx) { if (!st) return; + if (st->r.dyn_entries) { + for (size_t i = 0; i < st->r.n; i++) { + if (scr_stream_dyn_entry_traced(st->r.buf[i])) visit(st->r.buf[i], ctx); + } + } if (st->r.next_waiter) visit(st->r.next_waiter, ctx); if (st->r.read_cb) visit(st->r.read_cb, ctx); if (st->construct_cb) visit(st->construct_cb, ctx); @@ -348,6 +434,9 @@ 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)) + 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); } for (size_t i = 0; i < st->pipes.n; i++) visit(st->pipes.dst[i], ctx); for (size_t i = 0; i < st->drain_srcs.n; i++) visit(st->drain_srcs.src[i], ctx); @@ -425,6 +514,7 @@ typedef struct ScrStreamTick { ScrStreamTickOp op; ScrError *err; /* owned or NULL (SCR_ST_ERROR) */ ScrClosure *cb; /* owned or NULL (SCR_ST_AFTER_WRITE / SCR_ST_WCB_ERR / SCR_ST_WRITE_ERR) */ + bool hold; /* SCR_ST_AFTER_WRITE: counted in w.after_holds */ struct ScrStreamTick *next; } ScrStreamTick; @@ -486,10 +576,20 @@ static ScrError *scr_stream_mkerr(const char *code, const char *msg) { return e; } +/* The pending exception as a stream error (+1): Error values as + * themselves, any other thrown value as an internal assertion. */ +static ScrError *scr_stream_take_error(void) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *value = scr_caught_to_dyn(caught); + scr_caught_release(caught); + ScrError *err = value ? scr_errdyn_err_of(value) : NULL; + if (value) scr_dyn_release(value); + return err ? err : scr_stream_mkerr("ERR_INTERNAL_ASSERTION", "a stream operation threw a value that is not an Error"); +} + /* ── forward decls ────────────────────────────────────────────────────── */ static void scr_stream_flow(ScrStream *s); -static int scr_stream_add_entry(ScrStream *s, void *entry /*moves*/, bool is_str); static void scr_stream_readable_listen(ScrStream *s); static void scr_stream_emit_readable_now(ScrStream *s); static void scr_stream_maybe_read_more(ScrStream *s); @@ -498,7 +598,7 @@ static void scr_stream_clear_buffer(ScrStream *s); static void scr_stream_do_destroy(ScrStream *s, ScrError *err /*borrowed*/); static void scr_stream_notify_finished(ScrStream *s); static void *scr_stream_read_n(ScrStream *s, double size); -static void *scr_stream_read_n_kind(ScrStream *s, double size, bool *is_str); +static void *scr_stream_read_n_kind(ScrStream *s, double size, ScrEntryKind *kind); static void scr_stream_settle_next(ScrStream *s); static ScrStream *scr_stream_alloc(const ScrVt *vt, const char *cls, bool has_r, bool has_w, double rhwm, double whwm, bool auto_destroy, @@ -523,10 +623,10 @@ static void scr_stream_error_or_destroy(ScrStream *s, ScrError *err) { /* ── readable internals ───────────────────────────────────────────────── */ -static void scr_stream_rbuf_push(ScrStreamState *st, void *chunk /*moves*/, bool is_str, +static void scr_stream_rbuf_push(ScrStreamState *st, void *chunk /*moves*/, ScrEntryKind kind, bool front) { - if (is_str != st->r.encoded) { - /* every entry has the buffer's representation (encoded: ScrStr) */ + if (kind != scr_stream_rkind(st)) { + /* every entry has the buffer's representation (scr_stream_rkind) */ fputs("scriptc: internal error: stream buffer entry of the wrong kind\n", stderr); abort(); } @@ -549,7 +649,8 @@ static void scr_stream_rbuf_push(ScrStreamState *st, void *chunk /*moves*/, bool } /* Takes n units (n <= length; bytes, or chars once encoded) off the - * buffer head as one +1 Buffer / string. */ + * buffer head as one +1 Buffer / string. An object-mode buffer always + * takes one whole entry (n is 1). */ static void *scr_stream_rbuf_take(ScrStreamState *st, size_t n) { void *first = st->r.n > 0 ? st->r.buf[0] : NULL; if (first && st->r.head_off == 0 && scr_stream_entry_len(st, first) == n) { @@ -610,17 +711,29 @@ static void *scr_stream_rbuf_take(ScrStreamState *st, size_t n) { return out; } -/* emit('data', chunk) + pipe delivery. Borrows chunk (a Buffer, or a - * string once encoded). The emit ABI carries BOTH payload slots — the - * bytes chunk and the string chunk, exactly one non-NULL — and the - * compiler's stream-data listener thunks dispatch on which (typed - * listeners unwrap their declared side; dyn listeners box by tag). */ -static void scr_stream_emit_data(ScrStream *s, void *chunk) { +/* emit('data', chunk) + pipe delivery. Borrows chunk (a Buffer, a string + * once encoded, or a dyn value, by kind). The emit ABI carries three + * payload slots: the bytes chunk, the string chunk, and the value of a + * dyn entry. A dyn entry that is a Buffer or a string also fills that + * slot, so at most one of the first two is non-NULL; the compiler's + * stream-data listener thunks dispatch on them (typed listeners unwrap + * their declared side; dyn listeners box by tag or take the value). */ +static void scr_stream_emit_data(ScrStream *s, void *chunk, ScrEntryKind kind) { ScrStreamState *st = s->st; - ScrBytes *b = st->r.encoded ? NULL : (ScrBytes *)chunk; - ScrStr *str = st->r.encoded ? (ScrStr *)chunk : NULL; + ScrBytes *b = NULL; + ScrStr *str = NULL; + ScrDyn *value = NULL; + if (kind == SCR_ENTRY_DYN) { + value = chunk; + if (value->kind == SCR_DYN_BYTES) b = value->v.bytes; + else if (value->kind == SCR_DYN_STR) str = value->v.str; + } else if (kind == SCR_ENTRY_STR) { + str = chunk; + } else { + b = chunk; + } ScrStr *n = scr_str_new("data", 4); - scr_emitter_emit((ScrEmitter *)s, n, b, str); + scr_emitter_emit((ScrEmitter *)s, n, b, str, value); scr_str_release(n); if (scr_exc_pending()) return; /* pipe delivery, with backpressure: a full destination pauses us until @@ -637,7 +750,9 @@ static void scr_stream_emit_data(ScrStream *s, void *chunk) { if (scr_exc_pending()) return; continue; /* the pipes array shifted down over i */ } - bool ok = b ? scr_stream_write(dst, b, NULL) : scr_stream_write_str(dst, str, NULL); + bool ok = value ? scr_stream_write_dyn(dst, value, NULL) + : b ? scr_stream_write(dst, b, NULL) + : scr_stream_write_str(dst, str, NULL); if (scr_exc_pending()) return; i++; if (!ok && !dst->st->destroyed) { @@ -681,7 +796,7 @@ static void scr_stream_end_readable(ScrStream *s) { * ERR_METHOD_NOT_IMPLEMENTED, Node's contract. */ static void scr_stream_call_read(ScrStream *s) { ScrStreamState *st = s->st; - if (st->r.read_cb == NULL && st->r.object_entries) { + if (st->r.read_cb == NULL && st->r.object_entries && !st->r.object_mode_opt) { /* Readable.from: fully seeded at construction — _read is a no-op */ st->r.in_read_sync = false; return; @@ -716,8 +831,21 @@ static void scr_stream_call_read(ScrStream *s) { * StringDecoder first (Node decodes at push); an empty decode (a partial * multi-byte tail held pending) adds nothing but still clears the * reading flag. */ +static int scr_stream_add_entry(ScrStream *s, void *entry /*moves*/, ScrEntryKind kind); +static void *scr_stream_entry_convert(ScrStreamState *st, void *entry, ScrEntryKind *kind); + static bool scr_stream_add_chunk(ScrStream *s, ScrBytes *chunk, bool front) { ScrStreamState *st = s->st; + if (st->has_r && st->r.object_entries) { + /* object mode: the Buffer is one whole entry */ + if (!front) return scr_stream_add_entry(s, scr_bytes_retain(chunk), SCR_ENTRY_BYTES) > 0; + ScrEntryKind kind = SCR_ENTRY_BYTES; + void *entry = scr_stream_entry_convert(st, scr_bytes_retain(chunk), &kind); + scr_stream_rbuf_push(st, entry, kind, true); + if (st->r.need_readable) scr_stream_emit_readable_nt(s); + scr_stream_settle_next(s); + return !st->r.ended && (st->r.length < st->r.hwm || st->r.length == 0); + } if (!st->has_r) { ScrError *e = scr_stream_mkerr("ERR_STREAM_PUSH_AFTER_EOF", "push on a Writable (no readable side)"); scr_stream_error_or_destroy(s, e); @@ -753,12 +881,12 @@ static bool scr_stream_add_chunk(ScrStream *s, ScrBytes *chunk, bool front) { (scr_emitter_has((ScrEmitter *)s, "data") || st->pipes.n > 0)) { /* the direct-emit fast path: flowing, empty buffer, async push */ if (st->r.emitted_readable) st->r.emitted_readable = false; - bool is_str = st->r.encoded; - scr_stream_emit_data(s, entry); - scr_stream_entry_release_kind(entry, is_str); + ScrEntryKind kind = scr_stream_rkind(st); + scr_stream_emit_data(s, entry, kind); + scr_stream_entry_release_kind(entry, kind); if (scr_exc_pending()) return false; } else { - scr_stream_rbuf_push(st, entry, st->r.encoded, front); + scr_stream_rbuf_push(st, entry, scr_stream_rkind(st), front); if (st->r.need_readable) scr_stream_emit_readable_nt(s); } if (!front) scr_stream_maybe_read_more(s); @@ -781,8 +909,8 @@ static void scr_stream_maybe_read_more(ScrStream *s) { * Answers a +1 Buffer — or a +1 string once encoded (lengths and the * size argument then count JS string units, Node's encoded accounting). */ static void *scr_stream_read_n(ScrStream *s, double size) { - bool is_str; - return scr_stream_read_n_kind(s, size, &is_str); + ScrEntryKind kind; + return scr_stream_read_n_kind(s, size, &kind); } /* read_n, also answering the chunk's kind as it was taken from the buffer @@ -790,9 +918,9 @@ static void *scr_stream_read_n(ScrStream *s, double size) { * inside the read can change the representation, and a 'data' listener * can call setEncoding() before this returns, so callers that need the * kind read it here rather than from st->r.encoded afterwards. */ -static void *scr_stream_read_n_kind(ScrStream *s, double size, bool *is_str) { +static void *scr_stream_read_n_kind(ScrStream *s, double size, ScrEntryKind *kind) { ScrStreamState *st = s->st; - *is_str = false; + *kind = scr_stream_rkind(st); bool absent = size < 0; size_t n = 0; if (!absent) { @@ -852,7 +980,7 @@ static void *scr_stream_read_n_kind(ScrStream *s, double size, bool *is_str) { } } } - *is_str = st->r.encoded; + *kind = scr_stream_rkind(st); void *ret = want > 0 && want <= st->r.length ? scr_stream_rbuf_take(st, want) : NULL; if (ret == NULL) { st->r.need_readable = st->r.length <= st->r.hwm; @@ -865,9 +993,9 @@ static void *scr_stream_read_n_kind(ScrStream *s, double size, bool *is_str) { if (st->r.ended && absent) scr_stream_end_readable(s); } if (ret != NULL && !st->error_emitted && !st->close_emitted) { - scr_stream_emit_data(s, ret); + scr_stream_emit_data(s, ret, *kind); if (scr_exc_pending()) { - scr_stream_entry_release_kind(ret, *is_str); + scr_stream_entry_release_kind(ret, *kind); return NULL; } } @@ -877,10 +1005,10 @@ static void *scr_stream_read_n_kind(ScrStream *s, double size, bool *is_str) { static void scr_stream_flow(ScrStream *s) { ScrStreamState *st = s->st; while (st->r.flowing == 1 && !scr_exc_pending()) { - bool is_str; - void *b = scr_stream_read_n_kind(s, -1, &is_str); + ScrEntryKind kind; + void *b = scr_stream_read_n_kind(s, -1, &kind); if (b == NULL) break; - scr_stream_entry_release_kind(b, is_str); + scr_stream_entry_release_kind(b, kind); } } @@ -950,43 +1078,33 @@ ScrDyn *scr_stream_done_dyn_d(ScrClosure *clo, ScrDyn *const *args, size_t argc) return scr_stream_done_dyn_ret(); } -static void scr_stream_dyn_data(ScrDyn *const *args, size_t argc, - ScrBytes **data, ScrStr **data_str) { - *data = NULL; - *data_str = NULL; - if (argc < 2) return; - const ScrDyn *a = args[1]; - if (a->kind == SCR_DYN_BYTES) *data = scr_bytes_raw_view(a->v.bytes); - else if (a->kind == SCR_DYN_STR) *data_str = scr_str_retain(a->v.str); -} - +/* callback(err, data) of an untyped transform or flush: data is pushed + * as push(data) pushes it. */ ScrDyn *scr_stream_done_dyn_t(ScrClosure *clo, ScrDyn *const *args, size_t argc) { ScrStream *s = scr_stream_dyn_recv(clo); - ScrBytes *data; - ScrStr *data_str; - scr_stream_dyn_data(args, argc, &data, &data_str); - scr_stream_transform_done(s, scr_stream_dyn_err(args, argc), data, data_str); + scr_stream_transform_done_value(s, scr_stream_dyn_err(args, argc), argc > 1 ? args[1] : NULL); scr_stream_release(s); return scr_stream_done_dyn_ret(); } ScrDyn *scr_stream_done_dyn_l(ScrClosure *clo, ScrDyn *const *args, size_t argc) { ScrStream *s = scr_stream_dyn_recv(clo); - ScrBytes *data; - ScrStr *data_str; - scr_stream_dyn_data(args, argc, &data, &data_str); - scr_stream_flush_done(s, scr_stream_dyn_err(args, argc), data, data_str); + scr_stream_flush_done_value(s, scr_stream_dyn_err(args, argc), argc > 1 ? args[1] : NULL); scr_stream_release(s); return scr_stream_done_dyn_ret(); } /* Checked-dynamic chunks (the JS lane's push/write of any-typed values): * dispatch by dyn tag — bytes copy out, strings convert utf8, null takes - * the null path (EOF for push; ERR_STREAM_NULL_VALUES for write). Any - * other write throws Node's ERR_INVALID_ARG_TYPE; any other push throws - * the null-values TypeError (Node reports ERR_INVALID_ARG_TYPE through - * 'error'). Borrow d. */ + * the null path (EOF for push; ERR_STREAM_NULL_VALUES for write). On an + * object-mode side every other value is one entry. Otherwise any other + * write throws Node's ERR_INVALID_ARG_TYPE, push(undefined) adds nothing, + * and any other push reports ERR_INVALID_ARG_TYPE through 'error', as + * readableAddChunkPushByteMode does. Borrow d. */ bool scr_stream_push_dyn(ScrStream *s, const ScrDyn *d) { + ScrStreamState *st = s->st; + if (d->kind != SCR_DYN_NULL && st->has_r && st->r.object_entries) + return scr_stream_push_object(s, d) > 0; switch (d->kind) { case SCR_DYN_BYTES: { ScrBytes *b = scr_bytes_raw_view(d->v.bytes); @@ -998,15 +1116,26 @@ bool scr_stream_push_dyn(ScrStream *s, const ScrDyn *d) { return scr_stream_push_str(s, d->v.str); case SCR_DYN_NULL: return scr_stream_push_null(s); + case SCR_DYN_UNDEF: + if (!st->has_r) return false; + st->r.reading = false; + scr_stream_maybe_read_more(s); + return !st->r.ended && (st->r.length < st->r.hwm || st->r.length == 0); default: { - static const char msg[] = "May not write null values to stream"; - scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_STREAM_NULL_VALUES"); + scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", d); + ScrError *e = scr_stream_take_error(); + scr_stream_error_or_destroy(s, e); + scr_error_release(e); return false; } } } +static bool scr_stream_write_value(ScrStream *s, ScrDyn *v, ScrClosure *cb); + bool scr_stream_write_dyn(ScrStream *s, const ScrDyn *d, ScrClosure *cb) { + if (d->kind != SCR_DYN_NULL && s->st->has_w && s->st->w.object_mode) + return scr_stream_write_value(s, (ScrDyn *)d, cb); switch (d->kind) { case SCR_DYN_BYTES: { ScrBytes *b = scr_bytes_raw_view(d->v.bytes); @@ -1051,14 +1180,28 @@ static void scr_stream_next_str_fence(void) { scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1); } -/* Fulfills the parked iterator with a chunk taken from the buffer; is_str - * is its kind as taken. The typed lane's chunk is a Buffer, so a string - * chunk rejects with the encoding fence instead. */ +/* Fulfills the parked iterator with a chunk taken from the buffer; kind + * is its kind as taken. The typed lane's chunk is a Buffer, so another + * chunk rejects with the encoding fence instead. The dyn lane's end is + * the done marker (scr_stream_from_done): an object-mode stream can + * yield undefined. */ static void scr_stream_next_fulfill(ScrStreamState *st, ScrPromise *w /*moves*/, - void *chunk /*moves or NULL=EOF*/, bool is_str) { + void *chunk /*moves or NULL=EOF*/, ScrEntryKind kind) { void *payload; bool dyn = st->r.next_dyn; - if (!dyn && chunk != NULL && is_str) { + if (!dyn && chunk != NULL && kind == SCR_ENTRY_DYN) { + ScrDyn *d = chunk; + if (d->kind == SCR_DYN_BYTES) { + chunk = scr_bytes_raw_view(d->v.bytes); + kind = SCR_ENTRY_BYTES; + scr_dyn_release(d); + } else { + chunk = scr_dyn_string_coerce_js(d); + kind = SCR_ENTRY_STR; + scr_dyn_release(d); + } + } + if (!dyn && chunk != NULL && kind == SCR_ENTRY_STR) { scr_str_release((ScrStr *)chunk); scr_stream_next_str_fence(); scr_promise_reject_pending(w); @@ -1066,17 +1209,7 @@ static void scr_stream_next_fulfill(ScrStreamState *st, ScrPromise *w /*moves*/, return; } if (dyn) { - ScrDyn *d; - if (chunk == NULL) { - d = scr_dyn_retain(scr_dyn_undefined()); /* the EOF sentinel */ - } else if (is_str) { - d = scr_dyn_new_str((ScrStr *)chunk); - scr_str_release((ScrStr *)chunk); - } else { - d = scr_dyn_new_buffer((ScrBytes *)chunk); - scr_bytes_release((ScrBytes *)chunk); - } - payload = d; + 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); } @@ -1108,11 +1241,11 @@ static void scr_stream_iter_step(ScrStream *s) { if (!st->r.next_waiter) return; if (!st->destroyed && !st->errored) { st->r.next_reading = true; - bool is_str; - void *chunk = scr_stream_read_n_kind(s, -1, &is_str); + ScrEntryKind kind; + void *chunk = scr_stream_read_n_kind(s, -1, &kind); st->r.next_reading = false; if (scr_exc_pending()) { - if (chunk) scr_stream_entry_release_kind(chunk, is_str); + if (chunk) scr_stream_entry_release_kind(chunk, kind); ScrPromise *w = st->r.next_waiter; st->r.next_waiter = NULL; if (w) { @@ -1124,7 +1257,7 @@ static void scr_stream_iter_step(ScrStream *s) { if (chunk) { ScrPromise *w = st->r.next_waiter; st->r.next_waiter = NULL; - scr_stream_next_fulfill(st, w, chunk, is_str); + scr_stream_next_fulfill(st, w, chunk, kind); return; } } @@ -1175,13 +1308,18 @@ static void scr_stream_settle_next(ScrStream *s) { } ScrPromise *w = st->r.next_waiter; st->r.next_waiter = NULL; - bool is_str = st->r.encoded; + ScrEntryKind kind = scr_stream_rkind(st); void *chunk = scr_stream_rbuf_take(st, st->r.object_entries ? 1 : st->r.length); - scr_stream_next_fulfill(st, w, chunk, is_str); + scr_stream_next_fulfill(st, w, chunk, kind); if (st->r.ended && st->r.length == 0) scr_stream_end_readable(s); return; } if (st->r.ended || st->destroyed) { + if (st->r.ended && st->r.next_close_wait && !st->close_emitted) { + if (!st->r.end_emitted) scr_stream_end_readable(s); + st->r.next_eof_close = true; + return; + } if (st->r.ended && !st->r.end_emitted) { /* Node's iterator completes AFTER 'end' (and its autoDestroy): * queue the sentinel BEHIND the pending 'end' tick, so code after @@ -1192,7 +1330,7 @@ static void scr_stream_settle_next(ScrStream *s) { } ScrPromise *w = st->r.next_waiter; st->r.next_waiter = NULL; - scr_stream_next_fulfill(st, w, NULL, false); + scr_stream_next_fulfill(st, w, NULL, SCR_ENTRY_BYTES); } } @@ -1201,7 +1339,7 @@ static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn) { ScrPromise *p = scr_promise_new(); if (!st->has_r) { st->r.next_dyn = dyn; - scr_stream_next_fulfill(st, scr_promise_retain(p), NULL, false); + scr_stream_next_fulfill(st, scr_promise_retain(p), NULL, SCR_ENTRY_BYTES); return p; } if (st->r.next_waiter) { @@ -1214,6 +1352,11 @@ static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn) { st->r.next_dyn = dyn; st->r.next_waiter = scr_promise_retain(p); st->r.next_parked = false; + if (!st->r.next_started) { + st->r.next_started = true; + st->r.next_close_wait = + !st->has_w && st->auto_destroy && st->emit_close && !st->close_scheduled && !st->close_emitted; + } /* createAsyncIterator starts with stream.on('readable', next) */ scr_stream_readable_listen(s); scr_stream_iter_step(s); @@ -1268,25 +1411,11 @@ static void scr_from_src_release(ScrFromSrc *f) { static void *scr_from_src_retain_v(void *f) { return scr_from_src_retain(f); } static void scr_from_src_release_v(void *f) { scr_from_src_release(f); } -/* A Readable.from source whose values no longer fit the buffer's kind - * (strings beside Buffers): destroyed with the "not supported yet" Error. */ -static void scr_from_destroy_mixed(ScrStream *s) { - static const char m[] = "a Readable.from source that mixes strings and Buffers is not supported yet"; - ScrStr *msg = scr_str_new(m, sizeof m - 1); - ScrError *e = scr_error_new(SCR_ERR_ERROR, msg); - scr_str_release(msg); - scr_stream_release(scr_stream_destroy(s, e)); - scr_error_release(e); -} - /* One typed element into the stream: a whole entry in object mode, a * chunk in byte mode. Moves e. Returns push's answer. */ static bool scr_from_push_typed(ScrStream *s, void *e, bool strings) { - if (s->st->r.object_entries) { - int pushed = scr_stream_add_entry(s, e, strings); - if (pushed < 0) scr_from_destroy_mixed(s); - return pushed > 0; - } + if (s->st->r.object_entries) + return scr_stream_add_entry(s, e, strings ? SCR_ENTRY_STR : SCR_ENTRY_BYTES) > 0; bool more = strings ? scr_stream_push_str(s, e) : scr_stream_push(s, e); if (strings) scr_str_release(e); else scr_bytes_release(e); @@ -1447,10 +1576,10 @@ ScrStream *scr_stream_from_arr(ScrArr *arr, double flags, double hwm) { } /* The JS lane: one checked-dynamic value into the stream. null is - * ERR_STREAM_NULL_VALUES; in object mode, values other than strings and - * Buffers (or a mix of the two) are outside the stream model; byte mode - * rejects them with push()'s ERR_INVALID_ARG_TYPE. Each failure destroys - * the stream and answers -1. Borrows v. */ + * ERR_STREAM_NULL_VALUES; in object mode every other value is an entry; + * byte mode rejects values other than strings and Buffers with push()'s + * ERR_INVALID_ARG_TYPE. Each failure destroys the stream and answers -1. + * Borrows v. */ int scr_stream_from_push(ScrStream *s, const ScrDyn *v) { ScrError *e = NULL; if (v->kind == SCR_DYN_NULL) { @@ -1460,14 +1589,7 @@ int scr_stream_from_push(ScrStream *s, const ScrDyn *v) { scr_str_release(msg); scr_error_set_code(e, "ERR_STREAM_NULL_VALUES"); } else if (s->st->r.object_entries) { - int pushed = scr_stream_push_object(s, v); - if (pushed >= 0) return pushed; - static const char mixed[] = "a Readable.from source that mixes strings and Buffers is not supported yet"; - static const char other[] = "Readable.from over values other than strings and Buffers is not supported yet"; - const char *m = pushed == -2 ? mixed : other; - ScrStr *msg = scr_str_new(m, strlen(m)); - e = scr_error_new(SCR_ERR_ERROR, msg); - scr_str_release(msg); + return scr_stream_push_object(s, v); } else if (v->kind == SCR_DYN_STR || v->kind == SCR_DYN_BYTES) { return scr_stream_push_dyn(s, v) ? 1 : 0; } else { @@ -1598,61 +1720,72 @@ ScrStream *scr_stream_new_object_readable(ScrClosure *read, ScrStreamReadInv rea return s; } -/* An object-mode entry in the buffer's representation: a Buffer on a - * stream with a decoder decodes through it (Node's decoder.write in - * readableAddChunkPushObjectMode); an entry of the other kind otherwise - * mixes strings and Buffers, which the buffer cannot hold. Moves entry; - * +1, or NULL (entry released). */ -static void *scr_stream_entry_convert(ScrStreamState *st, void *entry, bool is_str) { - if (is_str == st->r.encoded) return entry; - if (!is_str && st->r.decoding) { +/* The object-mode buffer starts holding dyn values: every buffered entry + * is boxed. */ +static void scr_stream_rbuf_to_dyn(ScrStreamState *st) { + ScrEntryKind kind = scr_stream_rkind(st); + for (size_t i = 0; i < st->r.n; i++) st->r.buf[i] = scr_stream_entry_box(st->r.buf[i], kind); + st->r.dyn_entries = true; + st->r.object_kind_set = true; +} + +/* An object-mode entry in the buffer's representation (moves entry; +1). + * A Buffer on a stream with a decoder decodes through it (Node's + * decoder.write in readableAddChunkPushObjectMode); an entry of another + * kind turns the buffer into dyn entries. */ +static void *scr_stream_entry_convert(ScrStreamState *st, void *entry, ScrEntryKind *kind) { + ScrEntryKind want = scr_stream_rkind(st); + if (*kind == want) return entry; + if (*kind == SCR_ENTRY_BYTES && want == SCR_ENTRY_STR && st->r.decoding) { ScrBytes *b = entry; ScrStr *dec = scr_strdec_write(st->r.enc, st->r.dec_pending, b); st->r.dec_pending = scr_strdec_next(st->r.enc, st->r.dec_pending, b); scr_bytes_release(b); + *kind = SCR_ENTRY_STR; return dec; } - if (is_str) scr_str_release(entry); - else scr_bytes_release(entry); - return NULL; + if (want != SCR_ENTRY_DYN) scr_stream_rbuf_to_dyn(st); + ScrDyn *d = scr_stream_entry_box(entry, *kind); + *kind = SCR_ENTRY_DYN; + return d; } /* readableAddChunk in object mode: every value is one whole entry, empty - * strings and Buffers included. is_str gives entry's kind. Answers push()'s - * result, or -2 (entry released) when the kinds cannot mix. */ -static int scr_stream_add_entry(ScrStream *s, void *entry /*moves*/, bool is_str) { + * strings and Buffers included. kind gives entry's kind. Answers push()'s + * result. */ +static int scr_stream_add_entry(ScrStream *s, void *entry /*moves*/, ScrEntryKind kind) { ScrStreamState *st = s->st; if (!st->r.object_kind_set && st->r.length == 0) { /* the first entry picks the representation */ st->r.object_kind_set = true; - if (is_str && !st->r.encoded) { + if (kind == SCR_ENTRY_STR && !st->r.encoded) { st->r.encoded = true; if (!st->r.enc) st->r.enc = scr_str_new("utf8", 4); + } else if (kind == SCR_ENTRY_DYN) { + st->r.dyn_entries = true; } } - entry = scr_stream_entry_convert(st, entry, is_str); - if (entry == NULL) return -2; + entry = scr_stream_entry_convert(st, entry, &kind); if (st->r.ended) { - scr_stream_entry_release(st, entry); + scr_stream_entry_release_kind(entry, kind); ScrError *e = scr_stream_mkerr("ERR_STREAM_PUSH_AFTER_EOF", "stream.push() after EOF"); scr_stream_error_or_destroy(s, e); scr_error_release(e); return 0; } if (st->destroyed || st->errored) { - scr_stream_entry_release(st, entry); + scr_stream_entry_release_kind(entry, kind); return 0; } st->r.reading = false; if (st->r.flowing == 1 && st->r.length == 0 && !st->r.in_read_sync && (scr_emitter_has((ScrEmitter *)s, "data") || st->pipes.n > 0)) { if (st->r.emitted_readable) st->r.emitted_readable = false; - bool is_str = st->r.encoded; - scr_stream_emit_data(s, entry); - scr_stream_entry_release_kind(entry, is_str); + scr_stream_emit_data(s, entry, kind); + scr_stream_entry_release_kind(entry, kind); if (scr_exc_pending()) return 0; } else { - scr_stream_rbuf_push(st, entry, st->r.encoded, false); + scr_stream_rbuf_push(st, entry, kind, false); if (st->r.need_readable) scr_stream_emit_readable_nt(s); } scr_stream_maybe_read_more(s); @@ -1660,12 +1793,22 @@ static int scr_stream_add_entry(ScrStream *s, void *entry /*moves*/, bool is_str return !st->r.ended && (st->r.length < st->r.hwm || st->r.length == 0); } +/* push(value) on an object-mode stream: a string or a Buffer keeps its + * kind, any other value is a dyn entry. Borrows v. */ int scr_stream_push_object(ScrStream *s, const ScrDyn *v) { - bool is_str = v->kind == SCR_DYN_STR; - if (!is_str && v->kind != SCR_DYN_BYTES) return -1; - void *entry = is_str ? (void *)scr_str_retain(v->v.str) : (void *)scr_bytes_raw_view(v->v.bytes); - int pushed = scr_stream_add_entry(s, entry, is_str); - return pushed < 0 ? pushed : pushed ? 1 : 0; + void *entry; + ScrEntryKind kind; + if (v->kind == SCR_DYN_STR) { + entry = scr_str_retain(v->v.str); + kind = SCR_ENTRY_STR; + } else if (v->kind == SCR_DYN_BYTES) { + entry = scr_bytes_raw_view(v->v.bytes); + kind = SCR_ENTRY_BYTES; + } else { + entry = scr_dyn_retain((ScrDyn *)v); + kind = SCR_ENTRY_DYN; + } + return scr_stream_add_entry(s, entry, kind) ? 1 : 0; } /* ── the registration hook ('data'/'readable' listeners kick flow) ────── */ @@ -1706,7 +1849,8 @@ static void scr_stream_on_listener(ScrEmitter *em, ScrStr *name) { /* ── writable internals ───────────────────────────────────────────────── */ -static void scr_stream_wq_push(ScrStreamState *st, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/) { +static void scr_stream_wq_push(ScrStreamState *st, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/, + ScrDyn *enc /*moves*/) { if (st->w.n == st->w.cap) { st->w.cap = st->w.cap ? st->w.cap * 2 : 4; st->w.q = realloc(st->w.q, st->w.cap * sizeof *st->w.q); @@ -1714,29 +1858,49 @@ static void scr_stream_wq_push(ScrStreamState *st, ScrBytes *chunk /*moves*/, Sc } st->w.q[st->w.n].chunk = chunk; st->w.q[st->w.n].cb = cb; + st->w.q[st->w.n].enc = enc; st->w.n++; } static void scr_stream_transform_callback(ScrStream *s); -static void scr_stream_do_write(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/) { +static void scr_stream_do_write_inner(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/); + +/* One write reaching _write/_transform; the callback reads the write's + * encoding argument through w.cur_enc while it runs. */ +static void scr_stream_do_write(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/, + ScrDyn *enc /*moves*/) { + ScrStreamState *st = s->st; + if (enc == NULL) { + scr_stream_do_write_inner(s, chunk, cb); + return; + } + ScrDyn *outer = st->w.cur_enc; + st->w.cur_enc = enc; + scr_stream_do_write_inner(s, chunk, cb); + st->w.cur_enc = outer; + scr_dyn_release(enc); +} + +static void scr_stream_do_write_inner(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/) { ScrStreamState *st = s->st; st->w.writing = true; st->w.wsync = true; - st->w.inflight_len = chunk->len; + st->w.inflight_len = scr_stream_wchunk_len(st, chunk); st->w.inflight_cb = cb; if (st->is_transform || st->passthrough) st->xf_length = st->r.length; if (st->passthrough && st->transform_cb == NULL) { /* identity: push through, complete synchronously */ - scr_stream_add_chunk(s, chunk, false); - scr_bytes_release(chunk); + if (st->w.object_mode) 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); st->w.wsync = false; return; } if (st->is_transform || st->passthrough) { if (st->transform_cb == NULL) { - scr_bytes_release(chunk); + scr_stream_wchunk_release(st, chunk); ScrError *e = scr_stream_mkerr("ERR_METHOD_NOT_IMPLEMENTED", "The _transform() method is not implemented"); st->w.wsync = false; scr_stream_error_or_destroy(s, e); @@ -1744,12 +1908,12 @@ static void scr_stream_do_write(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClos return; } st->transform_inv(st->transform_cb, s, chunk); - scr_bytes_release(chunk); + scr_stream_wchunk_release(st, chunk); st->w.wsync = false; return; } if (st->w.write_cb == NULL) { - scr_bytes_release(chunk); + scr_stream_wchunk_release(st, chunk); ScrError *e = scr_stream_mkerr("ERR_METHOD_NOT_IMPLEMENTED", "The _write() method is not implemented"); st->w.wsync = false; scr_stream_error_or_destroy(s, e); @@ -1757,7 +1921,7 @@ static void scr_stream_do_write(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClos return; } st->w.write_inv(st->w.write_cb, s, chunk); - scr_bytes_release(chunk); + scr_stream_wchunk_release(st, chunk); st->w.wsync = false; } @@ -1770,8 +1934,9 @@ static void scr_stream_error_buffer(ScrStream *s) { if (st->w.writing) return; while (st->w.head < st->w.n && !scr_exc_pending()) { ScrWEntry e = st->w.q[st->w.head++]; - st->w.length -= e.chunk->len; - scr_bytes_release(e.chunk); + st->w.length -= scr_stream_wchunk_len(st, e.chunk); + scr_stream_wchunk_release(st, e.chunk); + if (e.enc) scr_dyn_release(e.enc); if (e.cb) { ((void (*)(ScrClosure *))e.cb->fn)(e.cb); scr_closure_release(e.cb); @@ -1863,12 +2028,14 @@ static SCR_COLD void scr_stream_writev_buffer(ScrStream *s) { } st->w.head = 0; st->w.n = 0; - scr_stream_do_write(s, all, cbs); + scr_stream_do_write(s, all, cbs, NULL); } /* 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; 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++]; @@ -1876,8 +2043,9 @@ static void scr_stream_clear_buffer_each(ScrStream *s) { st->w.head = 0; st->w.n = 0; } - scr_stream_do_write(s, e.chunk, e.cb); + scr_stream_do_write(s, e.chunk, e.cb, e.enc); } + st->w.buffer_processing = false; } /* The fs streams' clearBuffer: nothing while constructing, and _writev @@ -1947,6 +2115,11 @@ static void scr_stream_finish_maybe(ScrStream *s) { scr_stream_emit0(s, "prefinish"); if (scr_exc_pending()) return; } + /* prefinish ran; without a _final the deferred afterWrites still hold + * 'finish' (Node's pendingcb), and the last of them comes back here. + * A _final's callback puts 'finish' on a tick at once (callFinal's + * onFinish), and a Transform's _final is its flush. */ + if (st->w.after_holds > 0 && !st->w.final_cb && !st->is_transform && !st->passthrough) return; st->w.finish_scheduled = true; scr_st_tick(s, SCR_ST_FINISH, NULL, NULL); } @@ -2327,10 +2500,10 @@ static void scr_stream_dynopt_read_inv(ScrClosure *cb, ScrStream *s, double size static void scr_stream_dynopt_chunk3(ScrClosure *cb, ScrStream *s, ScrBytes *chunk, ScrDynThunk glue, const char *what) { ScrDyn *args[3]; - args[0] = scr_dyn_new_buffer(chunk); - ScrStr *enc = scr_str_new("buffer", 6); - args[1] = scr_dyn_new_str(enc); - scr_str_release(enc); + /* (chunk, encoding, cb): an object-mode side passes the value and the + * write's encoding argument, as Node's Writable does */ + args[0] = scr_stream_chunk_value(s, chunk); + args[1] = scr_stream_chunk_encoding_value(s); args[2] = scr_stream_dynopt_done(s, glue, 1, "(error)"); ScrDyn *fn = scr_stream_dynopt_target(cb); ScrDyn *r = scr_dyn_call(fn, args, 3, what); @@ -2345,10 +2518,8 @@ static void scr_stream_dynopt_write_inv(ScrClosure *cb, ScrStream *s, ScrBytes * static void scr_stream_dynopt_transform_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { ScrDyn *args[3]; - args[0] = scr_dyn_new_buffer(chunk); - ScrStr *enc = scr_str_new("buffer", 6); - args[1] = scr_dyn_new_str(enc); - scr_str_release(enc); + args[0] = scr_stream_chunk_value(s, chunk); + args[1] = scr_stream_chunk_encoding_value(s); args[2] = scr_stream_dynopt_done(s, &scr_stream_done_dyn_t, 2, "(error,data)"); ScrDyn *fn = scr_stream_dynopt_target(cb); ScrDyn *r = scr_dyn_call(fn, args, 3, "transform"); @@ -2398,6 +2569,7 @@ typedef struct { double rhwm, whwm; bool auto_destroy, emit_close, allow_half_open; bool readable_side, writable_side; + bool readable_object, writable_object; /* objectMode and the sided keys */ ScrStr *enc; /* owned canonical name, or NULL */ /* BORROWED callable fields (owned by the options record). */ ScrDyn *read, *write, *final_fn, *destroy, *transform, *flush; @@ -2474,17 +2646,17 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy out->readable_side = true; out->writable_side = true; out->enc = NULL; + out->readable_object = out->writable_object = false; out->read = out->write = out->final_fn = out->destroy = out->transform = out->flush = 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'; const bool r_sided = base != 'w'; - if (scr_stream_dynopt_truthy(opts, "objectMode", 10) || - scr_stream_dynopt_truthy(opts, "readableObjectMode", 18) || - scr_stream_dynopt_truthy(opts, "writableObjectMode", 18)) { - static const char m[] = "objectMode streams are not supported yet (chunks are Buffers or utf8 strings)"; - return scr_stream_dynopt_unsupported(m, sizeof m - 1); + { + bool object = scr_stream_dynopt_truthy(opts, "objectMode", 10); + 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)"; @@ -2527,7 +2699,7 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy /* highWaterMark wins over the sided keys (Node's getHighWaterMark); * the readable side is checked first, as Duplex's Readable constructor - * runs first. */ + * runs first. An object-mode side defaults to 16 entries. */ if (duplexish) { out->rhwm = scr_stream_dynopt_hwm(opts, "readableHighWaterMark", "options.readableHighWaterMark"); if (out->rhwm == -2) return false; @@ -2538,6 +2710,8 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy if (hwm == -2) return false; out->rhwm = out->whwm = hwm; } + if (out->rhwm == -1 && out->readable_object) out->rhwm = 16; + if (out->whwm == -1 && out->writable_object) out->whwm = 16; out->auto_destroy = !scr_stream_dynopt_false(scr_dyn_obj_get((ScrDyn *)opts, "autoDestroy", 11)); out->emit_close = !scr_stream_dynopt_false(scr_dyn_obj_get((ScrDyn *)opts, "emitClose", 9)); if (duplexish) { @@ -2578,6 +2752,10 @@ 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_apply_enc(ScrStream *s, ScrStr *enc) { if (enc == NULL) return; ScrStream *chain = scr_stream_set_encoding(s, enc); @@ -2593,6 +2771,7 @@ ScrStream *scr_stream_new_readable_dyn(ScrDyn *opts) { ScrClosure *destroy = scr_stream_dynopt_pick(o.destroy, (void *)&scr_stream_dynopt_destroy_inv, NULL, NULL, &dinv); ScrStream *s = scr_stream_new_readable(o.rhwm, o.auto_destroy, o.emit_close, read, (ScrStreamReadInv)rinv, destroy, (ScrStreamErrInv)dinv); + scr_stream_dynopt_apply_object(s, &o); scr_stream_dynopt_apply_enc(s, o.enc); return s; } @@ -2604,8 +2783,10 @@ ScrStream *scr_stream_new_writable_dyn(ScrDyn *opts) { ScrClosure *write = scr_stream_dynopt_pick(o.write, (void *)&scr_stream_dynopt_write_inv, NULL, NULL, &winv); ScrClosure *final_cb = scr_stream_dynopt_pick(o.final_fn, (void *)&scr_stream_dynopt_final_inv, NULL, NULL, &finv); ScrClosure *destroy = scr_stream_dynopt_pick(o.destroy, (void *)&scr_stream_dynopt_destroy_inv, NULL, NULL, &dinv); - return scr_stream_new_writable(o.whwm, o.auto_destroy, o.emit_close, + ScrStream *s = scr_stream_new_writable(o.whwm, o.auto_destroy, o.emit_close, write, (ScrStreamChunkInv)winv, final_cb, (ScrStreamPlainInv)finv, destroy, (ScrStreamErrInv)dinv); + scr_stream_dynopt_apply_object(s, &o); + return s; } ScrStream *scr_stream_new_duplex_dyn(ScrDyn *opts) { @@ -2620,6 +2801,7 @@ ScrStream *scr_stream_new_duplex_dyn(ScrDyn *opts) { o.allow_half_open, o.readable_side, o.writable_side, read, (ScrStreamReadInv)rinv, write, (ScrStreamChunkInv)winv, final_cb, (ScrStreamPlainInv)finv, destroy, (ScrStreamErrInv)dinv); + scr_stream_dynopt_apply_object(s, &o); scr_stream_dynopt_apply_enc(s, o.enc); return s; } @@ -2640,6 +2822,7 @@ static ScrStream *scr_stream_new_transformish_dyn(ScrDyn *opts, char base) { o.allow_half_open, o.readable_side, o.writable_side, transform, (ScrStreamChunkInv)tinv, flush, (ScrStreamPlainInv)linv, destroy, (ScrStreamErrInv)dinv); + scr_stream_dynopt_apply_object(s, &o); scr_stream_dynopt_apply_enc(s, o.enc); return s; } @@ -2666,6 +2849,7 @@ void scr_stream_init_readable_dyn(ScrStream *s, ScrDyn *opts, ScrClosure *destroy = scr_stream_dynopt_pick(o.destroy, (void *)&scr_stream_dynopt_destroy_inv, destroy_fb, (void *)destroy_fb_inv, &dinv); scr_stream_init_readable(s, o.rhwm, o.auto_destroy, o.emit_close, read, (ScrStreamReadInv)rinv, destroy, (ScrStreamErrInv)dinv); + scr_stream_dynopt_apply_object(s, &o); scr_stream_dynopt_apply_enc(s, o.enc); } @@ -2686,6 +2870,7 @@ void scr_stream_init_writable_dyn(ScrStream *s, ScrDyn *opts, ScrClosure *destroy = scr_stream_dynopt_pick(o.destroy, (void *)&scr_stream_dynopt_destroy_inv, destroy_fb, (void *)destroy_fb_inv, &dinv); scr_stream_init_writable(s, o.whwm, o.auto_destroy, o.emit_close, write, (ScrStreamChunkInv)winv, final_cb, (ScrStreamPlainInv)finv, destroy, (ScrStreamErrInv)dinv); + scr_stream_dynopt_apply_object(s, &o); } void scr_stream_init_duplex_dyn(ScrStream *s, ScrDyn *opts, @@ -2710,6 +2895,7 @@ void scr_stream_init_duplex_dyn(ScrStream *s, ScrDyn *opts, o.allow_half_open, o.readable_side, o.writable_side, read, (ScrStreamReadInv)rinv, write, (ScrStreamChunkInv)winv, final_cb, (ScrStreamPlainInv)finv, destroy, (ScrStreamErrInv)dinv); + scr_stream_dynopt_apply_object(s, &o); scr_stream_dynopt_apply_enc(s, o.enc); } @@ -2739,6 +2925,7 @@ static void scr_stream_init_transformish_dyn(ScrStream *s, ScrDyn *opts, char ba transform, (ScrStreamChunkInv)tinv, flush, (ScrStreamPlainInv)linv, destroy, (ScrStreamErrInv)dinv); } + scr_stream_dynopt_apply_object(s, &o); scr_stream_dynopt_apply_enc(s, o.enc); } @@ -3299,6 +3486,12 @@ static void scr_stream_done_marker_free(void) { scr_stream_done_marker = NULL; } +/* readable.chunkDone: whether a dyn for-await step answered the end (the + * done marker; an object-mode stream can yield undefined). */ +bool scr_stream_chunk_done(const ScrDyn *v) { + return v != NULL && v == scr_stream_done_marker; +} + ScrDyn *scr_stream_from_done(void) { if (!scr_stream_done_marker) { scr_stream_done_marker = scr_dyn_new_obj(); @@ -4067,25 +4260,20 @@ static void scr_compose_read_inv(ScrClosure *cb, ScrStream *s, double size) { } /* tail.on('data'): d.push(chunk), pausing the tail when d is full. The - * chunk is the tail's Buffer, or its string when the tail is encoded. */ -static void scr_compose_data_l(ScrClosure *cb, ScrBytes *bytes, ScrStr *str) { + * chunk is the tail's Buffer, its string when the tail is encoded, or an + * object-mode value. */ +static void scr_compose_data_l(ScrClosure *cb, ScrBytes *bytes, ScrStr *str, ScrDyn *value) { ScrCompose *c = scr_box_get_ref(cb->caps[0]); ScrStream *d = c->d; if (d) { ScrStreamState *dst = d->st; bool more; if (dst->r.object_entries) { - int pushed = scr_stream_add_entry( - d, str ? (void *)scr_str_retain(str) : (void *)scr_bytes_retain(bytes), str != NULL); - if (pushed < 0) { - static const char m[] = "a composed stream whose tail mixes strings and Buffers is not supported yet"; - ScrStr *msg = scr_str_new(m, sizeof m - 1); - ScrError *e = scr_error_new(SCR_ERR_ERROR, msg); - scr_str_release(msg); - scr_stream_release(scr_stream_destroy(d, e)); - scr_error_release(e); - } - more = pushed > 0; + more = value ? scr_stream_push_object(d, value) > 0 + : scr_stream_add_entry(d, str ? (void *)scr_str_retain(str) : (void *)scr_bytes_retain(bytes), + str ? SCR_ENTRY_STR : SCR_ENTRY_BYTES) > 0; + } else if (value && !str && !bytes) { + more = scr_stream_push_dyn(d, value); } else if (str) { more = scr_stream_push_str(d, str); } else { @@ -4206,7 +4394,7 @@ ScrStream *scr_stream_compose(double n_d, ScrStream **streams) { c->tail, scr_compose_closure((void *)&scr_compose_tail_eos, c), &scr_compose_tail_eos)); } if (c->readable) { - scr_compose_on(c->tail, "data", &c->data_l, (void *)&scr_compose_data_l, c, scr_ee_inv_fixed2); + scr_compose_on(c->tail, "data", &c->data_l, (void *)&scr_compose_data_l, c, scr_ee_inv_fixed3); scr_compose_on(c->tail, "end", &c->end_l, (void *)&scr_compose_end_l, c, scr_ee_inv_fixed0); } static SCR_TL bool registered = false; @@ -4379,7 +4567,21 @@ static void scr_sc_settle_ok(ScrClosure *cb, int kind) { /* The 'data' accumulation (the emit ABI's two payload slots — exactly * one non-NULL). */ -static void scr_sc_data(ScrClosure *cb, void *b, void *str) { +/* An object-mode value other than a string or a Buffer reaching a + * runtime consumer that reads text or bytes (stream/consumers, readline). */ +static void scr_stream_value_consumer_fence(const char *consumer) { + char msg[160]; + int n = snprintf(msg, sizeof msg, + "%s over an object-mode stream value that is not a string or Buffer is not supported yet", + consumer); + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)n, "SC2020"); +} + +static void scr_sc_data(ScrClosure *cb, void *b, void *str, void *value) { + if (b == NULL && str == NULL && value != NULL) { + scr_stream_value_consumer_fence("stream/consumers"); + return; + } ScrArr *chunks = scr_box_get_ref(cb->caps[1]); /* +1 */ if (b != NULL) { scr_arr_push_ref(chunks, scr_bytes_retain((ScrBytes *)b)); @@ -4452,7 +4654,7 @@ static ScrPromise *scr_sc_consume(ScrStream *s, int kind) { ScrStr *dn = scr_str_new("data", 4); scr_emitter_release(scr_emitter_on((ScrEmitter *)s, dn, scr_sc_closure((void *)&scr_sc_data, p, chunks), - scr_ee_inv_fixed2, false, false)); + scr_ee_inv_fixed3, false, false)); scr_str_release(dn); /* resume() rather than the 'data' hook alone: Node's consumer pulls * through the iterator, which drains a PAUSED stream too. */ @@ -4643,7 +4845,11 @@ static SCR_TL ScrRlHook *scr_rl_hooks = NULL; static double scr_stream_rl_id(ScrClosure *cb) { return scr_box_get_f64(cb->caps[0]); } -static void scr_stream_rl_data(ScrClosure *cb, void *b, void *str) { +static void scr_stream_rl_data(ScrClosure *cb, void *b, void *str, void *value) { + if (b == NULL && str == NULL && value != NULL) { + scr_stream_value_consumer_fence("readline"); + return; + } if (b != NULL) scr_rl_input_bytes(scr_stream_rl_id(cb), (ScrBytes *)b); else if (str != NULL) scr_rl_input_str(scr_stream_rl_id(cb), (ScrStr *)str); } @@ -4797,7 +5003,7 @@ double scr_stream_rl_open(ScrStream *in, ScrStream *out, bool out_stdout, double h->next = scr_rl_hooks; scr_rl_hooks = h; scr_stream_rl_on(in, "error", h->error, scr_ee_inv_fixed1); - scr_stream_rl_on(in, "data", h->data, scr_ee_inv_fixed2); + scr_stream_rl_on(in, "data", h->data, scr_ee_inv_fixed3); scr_stream_rl_on(in, "end", h->end, scr_ee_inv_fixed0); scr_stream_rl_resume(in); return id; @@ -5323,6 +5529,85 @@ void scr_console_bind(ScrDyn *console, double which, ScrStream *stream) { inst->stream_drop = &scr_console_stream_drop; } +/* ── object mode ──────────────────────────────────────────────────────── */ + +/* The objectMode option (both sides) or a Duplex's readableObjectMode and + * writableObjectMode, applied right after construction: the readable + * side's entries are whole values (each counts 1, and the first push picks + * the representation), and the writable side queues values. The caller + * passes the object-mode high-water marks (16 by default). */ +void scr_stream_set_object_mode(ScrStream *s, bool readable, bool writable) { + ScrStreamState *st = s->st; + if (readable && st->has_r) { + st->r.object_entries = true; + st->r.object_mode_opt = true; + } + if (writable && st->has_w) st->w.object_mode = true; +} + +/* stream.objectMode: scr_stream_set_object_mode, answering the receiver + * (+1). */ +ScrStream *scr_stream_object_mode(ScrStream *s, bool readable, bool writable) { + scr_stream_set_object_mode(s, readable, writable); + return scr_stream_retain(s); +} + +/* Whether the write and transform steps receive dyn values (an + * 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; +} + +/* emit('data', value) with a checked-dynamic chunk: the 'data' slots by + * the value's tag (scr_stream_emit_data's ABI). Borrows its arguments. */ +bool scr_stream_emit_value(ScrStream *s, ScrStr *name, ScrDyn *v) { + ScrBytes *b = v->kind == SCR_DYN_BYTES ? v->v.bytes : NULL; + ScrStr *str = v->kind == SCR_DYN_STR ? v->v.str : NULL; + return scr_emitter_emit((ScrEmitter *)s, name, b, str, v); +} + +/* The chunk a compiled write or transform callback receives, by its + * declared type: as a Buffer (+1; on an object-mode side the value must + * be a Buffer, else a TypeError is thrown and NULL answered), as a dyn + * value (+1), and the encoding argument: 'buffer' on a byte-mode side, + * else what the write passed (undefined, null or a name). A parameter + * typed string receives the name, or "" for undefined and null; one typed + * otherwise receives the value (+1 each). */ +ScrBytes *scr_stream_chunk_bytes(ScrStream *s, ScrBytes *chunk) { + if (!scr_stream_writes_values(s)) return scr_bytes_retain(chunk); + ScrDyn *v = (ScrDyn *)chunk; + if (v->kind == SCR_DYN_BYTES) return scr_bytes_raw_view(v->v.bytes); + static const char msg[] = + "a stream callback declaring a Buffer chunk received an object-mode value that is not a Buffer"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return NULL; +} + +ScrDyn *scr_stream_chunk_value(ScrStream *s, ScrBytes *chunk) { + if (scr_stream_writes_values(s)) return scr_dyn_retain((ScrDyn *)chunk); + return scr_dyn_new_buffer(chunk); +} + +ScrStr *scr_stream_chunk_encoding(ScrStream *s) { + if (!scr_stream_writes_values(s)) return scr_str_new("buffer", 6); + ScrDyn *enc = s->st->w.cur_enc; + if (enc && enc->kind == SCR_DYN_STR) return scr_str_retain(enc->v.str); + if (enc == NULL || enc->kind == SCR_DYN_NULL) return scr_str_new("", 0); + return scr_dyn_string_coerce_js(enc); +} + +ScrDyn *scr_stream_chunk_encoding_value(ScrStream *s) { + if (!scr_stream_writes_values(s)) { + ScrStr *name = scr_str_new("buffer", 6); + ScrDyn *d = scr_dyn_new_str(name); + scr_str_release(name); + return d; + } + ScrDyn *enc = s->st->w.cur_enc; + return enc ? scr_dyn_retain(enc) : scr_dyn_undefined(); +} + /* ── the readable surface ─────────────────────────────────────────────── */ bool scr_stream_push(ScrStream *s, ScrBytes *chunk) { @@ -5330,6 +5615,9 @@ bool scr_stream_push(ScrStream *s, ScrBytes *chunk) { } bool scr_stream_push_str(ScrStream *s, ScrStr *str) { + /* object mode: the string is one whole entry, not converted */ + if (s->st->has_r && s->st->r.object_entries) + return scr_stream_add_entry(s, scr_str_retain(str), SCR_ENTRY_STR) > 0; ScrStr *enc = s->st->has_r ? s->st->r.push_enc : NULL; ScrBytes *b; if (enc != NULL) { @@ -5374,11 +5662,11 @@ bool scr_stream_push_null(ScrStream *s) { if (!st->r.ended) { /* onEofChunk flushes the decoder's held partial first (utf8's * replacement chars et al — Node's decoder.end() push). */ - if (st->r.encoded) { + if (st->r.encoded && !st->r.dyn_entries) { ScrStr *tail = scr_strdec_end(st->r.enc, st->r.dec_pending); st->r.dec_pending = 0; if (scr_str_utf16_len(tail) > 0) { - scr_stream_rbuf_push(st, tail, true, false); + scr_stream_rbuf_push(st, tail, SCR_ENTRY_STR, false); if (st->r.need_readable) scr_stream_emit_readable_nt(s); } else { scr_str_release(tail); @@ -5408,7 +5696,7 @@ void scr_stream_unshift(ScrStream *s, ScrBytes *chunk) { if (st->r.head_off > 0) { /* materialize the part-read head so the front slot is whole */ void *rest = scr_stream_rbuf_take(st, scr_stream_entry_len(st, st->r.buf[0]) - st->r.head_off); - scr_stream_rbuf_push(st, rest, st->r.encoded, true); + scr_stream_rbuf_push(st, rest, scr_stream_rkind(st), true); } scr_stream_add_chunk(s, chunk, true); } @@ -5422,6 +5710,46 @@ void scr_stream_unshift_str(ScrStream *s, ScrStr *str) { scr_bytes_release(b); } +/* unshift(value): null is the end of the stream (Node's onEofChunk); an + * object-mode stream takes any other value as one entry at the front; a + * byte-mode stream takes Buffers and strings, ignores undefined and + * reports another value as push does. Borrows d. */ +void scr_stream_unshift_dyn(ScrStream *s, const ScrDyn *d) { + ScrStreamState *st = s->st; + if (d->kind == SCR_DYN_NULL) { + scr_stream_push_null(s); + return; + } + if (st->has_r && st->r.object_entries) { + void *entry; + ScrEntryKind kind; + if (d->kind == SCR_DYN_STR) { + entry = scr_str_retain(d->v.str); + kind = SCR_ENTRY_STR; + } else if (d->kind == SCR_DYN_BYTES) { + entry = scr_bytes_raw_view(d->v.bytes); + kind = SCR_ENTRY_BYTES; + } else { + entry = scr_dyn_retain((ScrDyn *)d); + kind = SCR_ENTRY_DYN; + } + entry = scr_stream_entry_convert(st, entry, &kind); + scr_stream_rbuf_push(st, entry, kind, true); + if (st->r.need_readable) scr_stream_emit_readable_nt(s); + scr_stream_settle_next(s); + return; + } + if (d->kind == SCR_DYN_BYTES) { + ScrBytes *b = scr_bytes_raw_view(d->v.bytes); + scr_stream_unshift(s, b); + scr_bytes_release(b); + } else if (d->kind == SCR_DYN_STR) { + scr_stream_unshift_str(s, d->v.str); + } else { + scr_stream_push_dyn(s, d); + } +} + static void scr_stream_listener_error(ScrClosure *adapter, ScrError *error) { ScrDyn *fn = scr_dyn_listener_fn(adapter); ScrDyn *arg = scr_dyn_from_error(error); @@ -5431,9 +5759,9 @@ static void scr_stream_listener_error(ScrClosure *adapter, ScrError *error) { scr_dyn_release(fn); } -static void scr_stream_listener_data(ScrClosure *adapter, ScrBytes *bytes, ScrStr *string) { +static void scr_stream_listener_data(ScrClosure *adapter, ScrBytes *bytes, ScrStr *string, ScrDyn *value) { ScrDyn *fn = scr_dyn_listener_fn(adapter); - ScrDyn *arg = string ? scr_dyn_new_str(string) : scr_dyn_new_bytes(bytes); + ScrDyn *arg = value ? scr_dyn_retain(value) : string ? scr_dyn_new_str(string) : scr_dyn_new_bytes(bytes); ScrDyn *answer = scr_dyn_call(fn, &arg, 1, "listener"); scr_dyn_release(answer); scr_dyn_release(arg); @@ -5444,7 +5772,7 @@ ScrStream *scr_stream_on_dyn(ScrStream *s, ScrStr *name, ScrDyn *cb, bool once, if (name->len == 4 && !memcmp(name->data, "data", 4)) { return (ScrStream *)scr_emitter_on_dyn((ScrEmitter *)s, name, cb, scr_dyn_listener_closure_fn(cb, (void *)scr_stream_listener_data), - scr_ee_inv_fixed2, once, prepend); + scr_ee_inv_fixed3, once, prepend); } bool error = name->len == 5 && memcmp(name->data, "error", 5) == 0; bool empty = (name->len == 8 && memcmp(name->data, "readable", 8) == 0) || @@ -5468,13 +5796,15 @@ ScrStream *scr_stream_on_dyn(ScrStream *s, ScrStr *name, ScrDyn *cb, bool once, ScrDyn *scr_stream_read_dyn(ScrStream *s, ScrDyn *size) { double n = size && size->kind != SCR_DYN_UNDEF ? scr_dyn_number_coerce(size) : -1; if (scr_exc_pending()) return scr_dyn_undefined(); - bool is_str = false; - void *chunk = s->st->has_r ? scr_stream_read_n_kind(s, n, &is_str) : NULL; + ScrEntryKind kind = SCR_ENTRY_BYTES; + void *chunk = s->st->has_r ? scr_stream_read_n_kind(s, n, &kind) : NULL; if (!chunk) return scr_dyn_new_null(); - ScrDyn *result = is_str ? scr_dyn_new_str(chunk) : scr_dyn_new_buffer(chunk); - if (is_str) scr_str_release(chunk); - else scr_bytes_release(chunk); - return result; + if (kind == SCR_ENTRY_BYTES) { + ScrDyn *result = scr_dyn_new_buffer(chunk); + scr_bytes_release(chunk); + return result; + } + return scr_stream_entry_box(chunk, kind); } /* read() answers STRINGS once encoded: the static result type here is @@ -5491,13 +5821,28 @@ ScrBytes *scr_stream_read(ScrStream *s, double size) { /* a destroyed readable still DRAINS its buffer (Node: destroy leaves * buffered chunks readable; only _read refills are blocked) */ if (!s->st->has_r) return NULL; - if (s->st->r.encoded) { + if (s->st->r.encoded && !s->st->r.dyn_entries) { scr_stream_read_str_fence(); return NULL; } - bool is_str; - void *chunk = scr_stream_read_n_kind(s, size, &is_str); - if (chunk && is_str) { + ScrEntryKind kind; + void *chunk = scr_stream_read_n_kind(s, size, &kind); + if (chunk && kind == SCR_ENTRY_DYN) { + /* an object-mode value: a Buffer is the result, anything else is not */ + ScrDyn *d = chunk; + if (d->kind == SCR_DYN_BYTES) { + ScrBytes *b = scr_bytes_raw_view(d->v.bytes); + scr_dyn_release(d); + return b; + } + scr_dyn_release(d); + static const char msg[] = + "read() of an object-mode stream value that is not a Buffer is not supported yet in typed " + "code (consume 'data' events or for await)"; + scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1); + return NULL; + } + if (chunk && kind == SCR_ENTRY_STR) { /* the read itself made the stream hold strings (a lazy Readable.from * pull's first string, or setEncoding() inside _read): the same * fence, never a string in the Buffer result */ @@ -5515,11 +5860,19 @@ ScrBytes *scr_stream_read(ScrStream *s, double size) { ScrStream *scr_stream_set_encoding(ScrStream *s, ScrStr *enc) { ScrStreamState *st = s->st; if (!st->has_r) return scr_stream_retain(s); + if (st->r.dyn_entries) { + /* an object-mode buffer of values: Buffers pushed later decode */ + if (st->r.enc) scr_str_release(st->r.enc); + st->r.enc = scr_str_retain(enc); + st->r.dec_pending = 0; + st->r.decoding = true; + return scr_stream_retain(s); + } if (st->r.encoded) { /* switching encodings mid-stream: fold the old decoder's pending * tail into the buffer first (rare; Node re-creates the decoder) */ ScrStr *tail = scr_strdec_end(st->r.enc, st->r.dec_pending); - if (scr_str_utf16_len(tail) > 0) scr_stream_rbuf_push(st, tail, true, false); + if (scr_str_utf16_len(tail) > 0) scr_stream_rbuf_push(st, tail, SCR_ENTRY_STR, false); else scr_str_release(tail); scr_str_release(st->r.enc); st->r.enc = scr_str_retain(enc); @@ -5575,7 +5928,7 @@ ScrStream *scr_stream_set_encoding(ScrStream *s, ScrStr *enc) { st->r.decoding = true; st->r.object_kind_set = true; if (content != NULL) { - if (scr_str_utf16_len(content) > 0) scr_stream_rbuf_push(st, content, true, false); + if (scr_str_utf16_len(content) > 0) scr_stream_rbuf_push(st, content, SCR_ENTRY_STR, false); else scr_str_release(content); } return scr_stream_retain(s); @@ -5647,7 +6000,8 @@ ScrStream *scr_stream_unpipe(ScrStream *src, ScrStream *dst) { /* ── the writable surface ─────────────────────────────────────────────── */ -static bool scr_stream_write_chunk(ScrStream *s, ScrBytes *chunk /*borrowed*/, ScrClosure *cb /*moves*/) { +static bool scr_stream_write_chunk_enc(ScrStream *s, ScrBytes *chunk /*borrowed*/, + ScrClosure *cb /*moves*/, ScrDyn *enc /*borrowed*/) { ScrStreamState *st = s->st; if (!st->has_w || st->destroyed || st->w.ending) { ScrError *e = st->has_w && !st->destroyed @@ -5658,12 +6012,12 @@ static bool scr_stream_write_chunk(ScrStream *s, ScrBytes *chunk /*borrowed*/, S scr_error_release(e); return false; } - size_t len = chunk->len; - st->w.length += len; + st->w.length += scr_stream_wchunk_len(st, chunk); + if (enc) enc = scr_dyn_retain(enc); if (st->w.writing || st->w.corked > 0 || st->constructing) { - scr_stream_wq_push(st, scr_bytes_retain(chunk), cb); + scr_stream_wq_push(st, scr_stream_wchunk_retain(st, chunk), cb, enc); } else { - scr_stream_do_write(s, scr_bytes_retain(chunk), cb); + scr_stream_do_write(s, scr_stream_wchunk_retain(st, chunk), cb, enc); } /* Node computes the answer AFTER the (possibly synchronous) write: * a sync completion has already drained its bytes from length. */ @@ -5672,7 +6026,28 @@ static bool scr_stream_write_chunk(ScrStream *s, ScrBytes *chunk /*borrowed*/, S return ret; } +static bool scr_stream_write_chunk(ScrStream *s, ScrBytes *chunk /*borrowed*/, ScrClosure *cb /*moves*/) { + return scr_stream_write_chunk_enc(s, chunk, cb, NULL); +} + +/* The encoding argument of a compiled write(chunk[, cb]) on an object-mode + * side: undefined, or null when the callback took its place (Node's + * write and end shift it there). */ +static ScrDyn *scr_stream_cb_enc(ScrClosure *cb) { return cb ? scr_dyn_new_null() : NULL; } + +/* write(value) on an object-mode writable side: the value is the chunk. + * Borrows v. */ +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)); +} + 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); + bool ret = scr_stream_write_value(s, v, cb); + scr_dyn_release(v); + return ret; + } return scr_stream_write_chunk(s, chunk, cb); } @@ -5686,6 +6061,13 @@ bool scr_stream_write_null(ScrStream *s) { } bool scr_stream_write_str(ScrStream *s, ScrStr *str, ScrClosure *cb) { + if (s->st->has_w && s->st->w.object_mode) { + /* object mode: the string itself, not converted */ + ScrDyn *v = scr_dyn_new_str(str); + bool ret = scr_stream_write_value(s, v, 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); @@ -5695,10 +6077,65 @@ bool scr_stream_write_str(ScrStream *s, ScrStr *str, ScrClosure *cb) { return ret; } +/* write(chunk, encoding, cb) with the encoding argument as passed (NULL + * or undefined when absent): an object-mode side hands it to _write + * unchanged; a byte-mode side encodes a string chunk with it. Borrows + * chunk and enc. */ +bool scr_stream_write_enc(ScrStream *s, const ScrDyn *chunk, ScrDyn *enc, ScrClosure *cb) { + if (enc && enc->kind == SCR_DYN_UNDEF) enc = NULL; + ScrStreamState *st = s->st; + if (chunk->kind != SCR_DYN_NULL && st->has_w && st->w.object_mode) + return scr_stream_write_chunk_enc(s, (ScrBytes *)chunk, cb, enc); + /* Node's _write: an encoding other than 'buffer' must be a known one + * (ERR_UNKNOWN_ENCODING, thrown), and a string chunk is encoded with + * it; an empty or nullish encoding is the default */ + ScrStr *name = NULL; + if (enc && enc->kind != SCR_DYN_NULL && !(enc->kind == SCR_DYN_STR && enc->v.str->len == 0)) + 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) { + 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); + return ret; + } + if (name && chunk->kind == SCR_DYN_STR) { + /* Buffer.from(string, 'buffer') */ + scr_str_release(name); + if (cb) scr_closure_release(cb); + static const char msg[] = "Unknown encoding: buffer"; + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_UNKNOWN_ENCODING"); + return false; + } + if (name) scr_str_release(name); + return scr_stream_write_dyn(s, chunk, cb); +} + ScrStream *scr_stream_end(ScrStream *s, ScrBytes *chunk_b, ScrStr *chunk_s, ScrClosure *cb) { ScrStreamState *st = s->st; - if (chunk_b) scr_stream_write(s, chunk_b, NULL); - else if (chunk_s) scr_stream_write_str(s, chunk_s, NULL); + if ((chunk_b || chunk_s) && st->has_w && st->w.object_mode) { + /* end(chunk[, cb]) writes with the encoding Node's end leaves: + * undefined, or null when the callback took its place */ + ScrDyn *v = chunk_s ? scr_dyn_new_str(chunk_s) + : chunk_b->is_buffer ? scr_dyn_new_buffer(chunk_b) : scr_dyn_new_bytes(chunk_b); + scr_stream_write_chunk_enc(s, (ScrBytes *)v, NULL, scr_stream_cb_enc(cb)); + scr_dyn_release(v); + } else if (chunk_b) { + scr_stream_write(s, chunk_b, NULL); + } else if (chunk_s) { + scr_stream_write_str(s, chunk_s, NULL); + } if (!st->has_w || st->destroyed) { if (cb) scr_closure_release(cb); return scr_stream_retain(s); @@ -5778,7 +6215,12 @@ void scr_stream_write_done(ScrStream *s, ScrError *err /*moves*/) { return; } if (was_sync) { + bool hold = ucb != NULL || (st->w.need_drain && st->w.length == 0) || st->destroyed; scr_st_tick(s, SCR_ST_AFTER_WRITE, NULL, ucb); + if (hold) { + scr_st_tail->hold = true; + st->w.after_holds++; + } } else { scr_stream_after_write(s, ucb); } @@ -5837,6 +6279,8 @@ void scr_stream_transform_done(ScrStream *s, ScrError *err /*moves*/, ScrBytes * if (!err) { if (data) { scr_stream_add_chunk(s, data, false); + } else if (data_str && s->st->r.object_entries) { + scr_stream_push_str(s, data_str); /* object mode: the string itself */ } else if (data_str) { ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)data_str->len); memcpy(b->data, data_str->data, data_str->len); @@ -5870,6 +6314,9 @@ void scr_stream_flush_done(ScrStream *s, ScrError *err /*moves*/, ScrBytes *data if (data) { scr_stream_add_chunk(s, data, false); scr_bytes_release(data); + } else if (data_str && st->r.object_entries) { + scr_stream_push_str(s, data_str); /* object mode: the string itself */ + scr_str_release(data_str); } else if (data_str) { ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)data_str->len); memcpy(b->data, data_str->data, data_str->len); @@ -5885,6 +6332,38 @@ void scr_stream_flush_done(ScrStream *s, ScrError *err /*moves*/, ScrBytes *data scr_stream_finish_maybe(s); } +/* The transform and flush completions with a checked-dynamic value (an + * object-mode Transform's data, or an untyped callback's): null and + * undefined push nothing (Transform's `val != null`), and any other value + * is pushed as push(value) pushes it. v borrowed (NULL: absent). */ +static bool scr_stream_done_pushes(const ScrDyn *v) { + return v && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_UNDEF; +} + +void scr_stream_transform_done_value(ScrStream *s, ScrError *err /*moves*/, ScrDyn *v) { + if (!err && scr_stream_done_pushes(v)) scr_stream_push_dyn(s, v); + if (scr_exc_pending()) { + if (err) scr_error_release(err); + return; + } + if (err) scr_stream_write_done(s, err); + else scr_stream_transform_callback(s); +} + +void scr_stream_flush_done_value(ScrStream *s, ScrError *err /*moves*/, ScrDyn *v) { + s->st->flush_pending = false; + if (err) { + scr_stream_error_or_destroy(s, err); + scr_error_release(err); + return; + } + if (scr_stream_done_pushes(v)) scr_stream_push_dyn(s, v); + if (scr_exc_pending()) return; + scr_stream_push_null(s); + if (scr_exc_pending()) return; + scr_stream_finish_maybe(s); +} + /* ── destroy ──────────────────────────────────────────────────────────── */ /* Node's _destroy step: the destroy hook, or 'error'/'close' on ticks. */ @@ -5970,6 +6449,7 @@ double scr_stream_prop(ScrStream *s, const char *name) { if (strcmp(name, "readableLength") == 0) return (double)st->r.length; if (strcmp(name, "readableHighWaterMark") == 0) return (double)st->r.hwm; if (strcmp(name, "readableObjectMode") == 0) return st->r.object_entries; + if (strcmp(name, "writableObjectMode") == 0) return st->w.object_mode; if (strcmp(name, "writable") == 0) { return st->has_w && !st->destroyed && !st->w.ending && !st->errored; } @@ -6011,7 +6491,7 @@ double scr_stream_prop(ScrStream *s, const char *name) { if (strcmp(p, "destroyed") == 0) return st->destroyed; if (strcmp(p, "errorEmitted") == 0) return st->error_emitted; if (strcmp(p, "needDrain") == 0) return st->w.need_drain; - if (strcmp(p, "objectMode") == 0) return 0; + if (strcmp(p, "objectMode") == 0) return st->w.object_mode; if (strcmp(p, "constructed") == 0) return 1; if (strcmp(p, "closed") == 0) return st->close_emitted; if (strcmp(p, "length") == 0) return (double)st->w.length; @@ -6209,6 +6689,7 @@ static void scr_stream_run_tick(ScrStreamTick *t) { break; case SCR_ST_AFTER_WRITE: { st->w.after_scheduled = false; + if (t->hold) st->w.after_holds--; ScrClosure *cb = t->cb; t->cb = NULL; if (!st->destroyed) { @@ -6272,6 +6753,13 @@ static void scr_stream_run_tick(ScrStreamTick *t) { if (!st->close_emitted) { st->close_emitted = true; if (st->emit_close) scr_stream_emit0(s, "close"); + if (st->r.next_eof_close && !scr_exc_pending()) { + /* the iterator's eos 'close' listener completes the loop */ + st->r.next_eof_close = false; + ScrPromise *w = st->r.next_waiter; + st->r.next_waiter = NULL; + if (w) scr_stream_next_fulfill(st, w, NULL, SCR_ENTRY_BYTES); + } /* eos ordering: the finished()/pipeline watchers fire right after * 'close' (Node's willEmitClose stance). */ scr_stream_notify_finished(s); @@ -6318,9 +6806,9 @@ static void scr_stream_run_tick(ScrStreamTick *t) { } case SCR_ST_READ0: { if (st->has_r) { - bool is_str; - void *chunk = scr_stream_read_n_kind(s, 0, &is_str); - if (chunk) scr_stream_entry_release_kind(chunk, is_str); + ScrEntryKind kind; + void *chunk = scr_stream_read_n_kind(s, 0, &kind); + if (chunk) scr_stream_entry_release_kind(chunk, kind); } break; } @@ -6347,10 +6835,13 @@ static void scr_stream_run_tick(ScrStreamTick *t) { break; } case SCR_ST_NEXT_EOF: { - if (st->r.next_waiter && st->r.length == 0 && (st->r.ended || st->destroyed)) { + if (st->r.next_waiter && st->r.length == 0 && st->r.ended && st->r.next_close_wait && + !st->close_emitted) { + st->r.next_eof_close = true; + } else if (st->r.next_waiter && st->r.length == 0 && (st->r.ended || st->destroyed)) { ScrPromise *w = st->r.next_waiter; st->r.next_waiter = NULL; - scr_stream_next_fulfill(st, w, NULL, false); + scr_stream_next_fulfill(st, w, NULL, SCR_ENTRY_BYTES); } else if (st->r.next_waiter) { scr_stream_settle_next(s); /* content raced in: deliver it */ } @@ -6475,16 +6966,6 @@ static const ScrDynHandleTransform *scr_handle_step_ops(ScrClosure *cb) { return scr_handle_step_handle_ops(cb)->transform; } -/* The pending exception as a stream error (+1). */ -static ScrError *scr_handle_step_caught(void) { - ScrCaught *caught = scr_exc_take(); - ScrDyn *value = scr_caught_to_dyn(caught); - scr_caught_release(caught); - ScrError *err = value ? scr_errdyn_err_of(value) : NULL; - if (value) scr_dyn_release(value); - return err ? err : scr_stream_mkerr("ERR_INTERNAL_ASSERTION", "a Transform handle threw a value that is not an Error"); -} - static void scr_handle_step_transform(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { /* A throwing update() leaves the exception pending: it throws out of * the write, as Node's _transform does. */ @@ -6494,7 +6975,7 @@ static void scr_handle_step_transform(ScrClosure *cb, ScrStream *s, ScrBytes *ch static void scr_handle_step_flush(ScrClosure *cb, ScrStream *s) { ScrBytes *out = scr_handle_step_ops(cb)->final(scr_handle_step_handle(cb)); - scr_stream_flush_done(s, out ? NULL : scr_handle_step_caught(), out, NULL); + scr_stream_flush_done(s, out ? NULL : scr_stream_take_error(), out, NULL); } static void scr_handle_step_destroy(ScrClosure *cb, ScrStream *s, ScrError *err) { diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 1e5b789e71..8b8a72406c 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -272,6 +272,7 @@ ScrStream *scr_stream_set_push_encoding(ScrStream *s, ScrStr *enc); bool scr_stream_push_null(ScrStream *s); void scr_stream_unshift(ScrStream *s, ScrBytes *chunk); void scr_stream_unshift_str(ScrStream *s, ScrStr *str); +void scr_stream_unshift_dyn(ScrStream *s, const ScrDyn *d); ScrBytes *scr_stream_read(ScrStream *s, double size); ScrStream *scr_stream_on_dyn(ScrStream *s, ScrStr *name, ScrDyn *cb, bool once, bool prepend); ScrDyn *scr_stream_read_dyn(ScrStream *s, ScrDyn *size); /* +1 or NULL; size < 0 = all/first; throws once encoded (the static type is Buffer|null) */ @@ -291,6 +292,8 @@ typedef struct ScrPromise ScrPromise; /* full declaration below (C11 repeat) */ * each chunk before asking again). */ bool scr_stream_push_dyn(ScrStream *s, const ScrDyn *d); /* borrows d; tag dispatch */ bool scr_stream_write_dyn(ScrStream *s, const ScrDyn *d, ScrClosure *cb); /* cb moves */ +/* write(chunk, encoding, cb): enc borrowed, NULL or undefined when absent */ +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); /* node:stream/promises — the promise forms over the finished/pipeline @@ -475,6 +478,28 @@ void scr_stream_final_done(ScrStream *s, ScrError *err); void scr_stream_destroy_done(ScrStream *s, ScrError *err); void scr_stream_transform_done(ScrStream *s, ScrError *err, ScrBytes *data, ScrStr *data_str); void scr_stream_flush_done(ScrStream *s, ScrError *err, ScrBytes *data, ScrStr *data_str); +/* The same completions with a checked-dynamic data value (borrowed; NULL, + * null and undefined push nothing): an object-mode Transform's data, or an + * untyped callback's. err moves. */ +/* objectMode: scr_stream_set_object_mode applies the option's sides right + * after construction; scr_stream_writes_values answers whether the write + * and transform steps receive dyn values instead of Buffers. */ +void scr_stream_set_object_mode(ScrStream *s, bool readable, bool writable); +ScrStream *scr_stream_object_mode(ScrStream *s, bool readable, bool writable); +bool scr_stream_writes_values(ScrStream *s); +/* A compiled write or transform callback's chunk by its declared type: a + * Buffer (+1, or NULL with a TypeError for another object-mode value), a + * dyn value (+1), and the encoding argument (+1). */ +ScrBytes *scr_stream_chunk_bytes(ScrStream *s, ScrBytes *chunk); +struct ScrDyn *scr_stream_chunk_value(ScrStream *s, ScrBytes *chunk); +ScrStr *scr_stream_chunk_encoding(ScrStream *s); +ScrDyn *scr_stream_chunk_encoding_value(ScrStream *s); +/* emit('data', value) with a checked-dynamic chunk (borrowed). */ +bool scr_stream_emit_value(ScrStream *s, ScrStr *name, struct ScrDyn *v); +/* Whether a dyn for-await step (readable.nextChunkDyn) answered the end. */ +bool scr_stream_chunk_done(const struct ScrDyn *v); +void scr_stream_transform_done_value(ScrStream *s, ScrError *err, struct ScrDyn *v); +void scr_stream_flush_done_value(ScrStream *s, ScrError *err, struct ScrDyn *v); /* Runtime-backed streams (the fs streams in scr_fs_stream.c). * - construct: Node's _construct. The hook runs on a tick after diff --git a/packages/runtime/src/scr_stream_ops.c b/packages/runtime/src/scr_stream_ops.c index d32dd5d67a..8fd2201680 100644 --- a/packages/runtime/src/scr_stream_ops.c +++ b/packages/runtime/src/scr_stream_ops.c @@ -305,7 +305,7 @@ static ScrPromise *src_next(OpSrc *src) { } static bool src_done(const OpSrc *src, const ScrDyn *v) { - return src->s ? v->kind == SCR_DYN_UNDEF : v == op_k_done; + return src->s ? scr_stream_chunk_done(v) : v == op_k_done; } /* The Readable iterator's return(): the iterator generator resumes at its @@ -1492,16 +1492,6 @@ static void op_from_step_k(void *self, ScrDyn *v) { } int pushed = scr_stream_push_object(out, v); scr_dyn_release(v); - if (pushed == -2) { - op_from_destroy_with(out, op_unsupported( - "a stream operator output that mixes strings and Buffers")); - return; - } - if (pushed < 0) { - op_from_destroy_with(out, op_unsupported( - "a stream operator value that is not a string or Buffer (object-mode streams of other values)")); - return; - } if (pushed) { from_next(f, out); return; diff --git a/tests/corpus/7240-objectmode-streams.ts b/tests/corpus/7240-objectmode-streams.ts new file mode 100644 index 0000000000..9316ff4802 --- /dev/null +++ b/tests/corpus/7240-objectmode-streams.ts @@ -0,0 +1,130 @@ +// Typed object-mode streams: records through Readable.from, Transform +// and Writable in pipeline(), typed 'data' listeners, a Transform with +// readableObjectMode parsing bytes into records, an object-mode Duplex, +// a PassThrough's object-counted high-water mark, and Readable.from over +// a Map, a Set of records and an array mixing strings and Buffers. +import { Duplex, PassThrough, Readable, Transform, Writable } from "node:stream"; +import { pipeline } from "node:stream/promises"; + +interface Row { + id: number; + name: string; +} + +interface Total { + count: number; + ids: number[]; +} + +async function records(): Promise { + const source = Readable.from([ + { id: 1, name: "ada" }, + { id: 2, name: "bo" }, + { id: 3, name: "cy" }, + ]); + const shout = new Transform({ + objectMode: true, + transform(row: Row, _enc: string, cb: (err: Error | null, out?: Row) => void) { + if (row.id === 2) { + cb(null); + return; + } + cb(null, { id: row.id * 10, name: row.name.toUpperCase() }); + }, + }); + const seen: Row[] = []; + await pipeline( + source, + shout, + new Writable({ + objectMode: true, + write(row: Row, enc: string | null | undefined, cb: () => void) { + seen.push(row); + console.log("write", row.id, enc); + cb(); + }, + }), + ); + console.log(seen.map((r) => `${r.id}:${r.name}`).join(" ")); +} + +function counters(): void { + const counter = new Readable({ objectMode: true, read() {} }); + console.log("hwm", counter.readableHighWaterMark, counter.readableObjectMode); + counter.on("data", (n: number) => console.log("data", n + 1)); + counter.on("end", () => console.log("counter end")); + counter.push(1); + counter.push(0); + counter.push(41); + counter.push(null); +} + +function sided(): void { + // bytes in, records out + const parse = new Transform({ + readableObjectMode: true, + transform(chunk: Buffer, _enc: string, cb: (err: Error | null, out?: Total) => void) { + const ids = chunk.toString().split(",").map((s) => Number(s)); + cb(null, { count: ids.length, ids }); + }, + }); + console.log( + "sided", + parse.readableObjectMode, + parse.writableObjectMode, + parse.readableHighWaterMark, + parse.writableHighWaterMark, + ); + parse.on("data", (t: Total) => console.log("total", t.count, t.ids.join("+"))); + parse.write("1,2,3"); + parse.end("4"); +} + +function duplex(): void { + const echo = new Duplex({ + objectMode: true, + read() {}, + write(chunk: Row, _enc: string, cb: () => void) { + this.push({ id: chunk.id + 1, name: chunk.name }); + cb(); + }, + final(cb: () => void) { + this.push(null); + cb(); + }, + }); + echo.on("data", (r: Row) => console.log("echo", r.id, r.name)); + echo.write({ id: 1, name: "x" }); + echo.end({ id: 5, name: "y" }); +} + +function passthrough(): void { + const pt = new PassThrough({ objectMode: true, highWaterMark: 2 }); + const ok1 = pt.write({ id: 1, name: "a" }); + const ok2 = pt.write({ id: 2, name: "b" }); + console.log("backpressure", ok1, ok2, pt.writableLength, pt.readableLength); + pt.on("data", (r: Row) => console.log("pt", r.name)); + pt.end(); +} + +async function collections(): Promise { + const prices = new Map([ + ["tea", 3], + ["cake", 5], + ]); + for await (const entry of Readable.from(prices)) console.log("entry", JSON.stringify(entry)); + const mixed: (string | Buffer)[] = ["text", Buffer.from("bytes")]; + for await (const chunk of Readable.from(mixed)) console.log("mixed", typeof chunk, String(chunk)); + const tags = new Set([{ tag: "a" }, { tag: "b" }]); + for await (const t of Readable.from(tags)) console.log("set", JSON.stringify(t)); +} + +async function main(): Promise { + await records(); + await collections(); + counters(); + sided(); + duplex(); + passthrough(); +} +main(); diff --git a/tests/corpus/7241-objectmode-streams.mjs b/tests/corpus/7241-objectmode-streams.mjs new file mode 100644 index 0000000000..334273c002 --- /dev/null +++ b/tests/corpus/7241-objectmode-streams.mjs @@ -0,0 +1,148 @@ +// Object-mode streams in JavaScript: any value as a chunk through +// Readable, Writable, Duplex, Transform and PassThrough, pipe() and +// pipeline() between them, unshift(), the per-side modes and +// their high-water marks, and the encoding argument write() and end() +// hand to _write and _transform. +import { Duplex, PassThrough, Readable, Transform, Writable } from "node:stream"; +import { pipeline } from "node:stream/promises"; + +const show = (v) => (typeof v === "object" ? JSON.stringify(v) : `${typeof v}:${String(v)}`); + +async function values() { + const seen = []; + await pipeline( + Readable.from([1, "two", { three: 3 }, [4], true, 0, ""]), + new Transform({ + objectMode: true, + transform(chunk, enc, cb) { + cb(null, { wrapped: chunk }); + }, + }), + new Writable({ + objectMode: true, + write(chunk, enc, cb) { + seen.push(show(chunk.wrapped)); + cb(); + }, + }), + ); + console.log("values:", seen.join(" ")); +} + +function lines() { + let rest = ""; + const split = new Transform({ + readableObjectMode: true, + transform(chunk, enc, cb) { + const parts = (rest + chunk.toString()).split("\n"); + rest = parts.pop(); + for (const p of parts) this.push({ line: p, length: p.length }); + cb(); + }, + flush(cb) { + if (rest) this.push({ line: rest, length: rest.length }); + cb(); + }, + }); + console.log("split modes:", split.readableObjectMode, split.writableObjectMode); + split.on("data", (row) => console.log("line:", row.line, row.length)); + split.on("end", () => console.log("split end")); + split.write("alpha\nbe"); + split.write("ta\ngamma"); + split.end(); +} + +function encodings() { + const w = new Writable({ + objectMode: true, + write(chunk, enc, cb) { + console.log("write enc:", show(chunk), enc); + cb(); + }, + }); + w.write(1, () => console.log("write cb")); + w.write(2, null); + w.write(3, "utf8"); + w.write("four", "latin1"); + w.write(5, "anything"); + w.end(6, () => console.log("end cb")); + const t = new Transform({ + objectMode: true, + transform(chunk, enc, cb) { + console.log("transform enc:", show(chunk), enc); + cb(); + }, + }); + t.write(7); + t.end(8, "hex"); + const bytes = new Writable({ + write(chunk, enc, cb) { + console.log("byte write:", chunk.toString("hex"), enc); + cb(); + }, + }); + bytes.write("hi", "latin1"); + bytes.write("6869", "hex"); + try { + bytes.write("x", "bogus"); + } catch (e) { + console.log("byte encoding:", e.code); + } + bytes.end(); +} + +function unshiftFront() { + const r = new Readable({ objectMode: true, read() {} }); + r.push({ id: 1 }); + r.push({ id: 2 }); + r.push(null); + console.log("length:", r.readableLength, "hwm:", r.readableHighWaterMark); + r.unshift({ id: 0 }); + r.on("data", (v) => console.log("after unshift:", show(v))); + r.on("end", () => console.log("read end")); +} + +function piped() { + const src = new PassThrough({ objectMode: true }); + const echo = new Duplex({ + objectMode: true, + read() {}, + write(chunk, enc, cb) { + this.push({ echoed: chunk }); + cb(); + }, + final(cb) { + this.push(null); + cb(); + }, + }); + src.pipe(echo).on("data", (v) => console.log("piped:", show(v))); + src.write({ n: 1 }); + src.write([2, 3]); + src.end("last"); +} + +function byteModeValues() { + const r = new Readable({ read() {} }); + r.on("error", (e) => console.log("byte push:", e.code)); + r.push(undefined); + r.push(42); +} + +async function forAwait() { + const pt = new PassThrough({ objectMode: true, highWaterMark: 2 }); + const ok = [pt.write(1), pt.write(2), pt.write(3)]; + console.log("backpressure:", ok.join(" "), pt.writableLength); + pt.end({ done: true }); + const got = []; + for await (const v of pt) got.push(show(v)); + console.log("for await:", got.join(" ")); +} + +await values(); +lines(); +encodings(); +unshiftFront(); +piped(); +byteModeValues(); +await forAwait(); diff --git a/tests/corpus/7242-objectmode-dynamic.cjs b/tests/corpus/7242-objectmode-dynamic.cjs new file mode 100644 index 0000000000..fad06ad980 --- /dev/null +++ b/tests/corpus/7242-objectmode-dynamic.cjs @@ -0,0 +1,93 @@ +// Object-mode streams through dynamic values: options objects held in +// variables, constructor functions with util.inherits (Transform.call +// and Writable.call with objectMode), streams passed around as plain +// values, and Readable.from over values of mixed types. +const stream = require("stream"); +const util = require("util"); + +const show = (v) => (v !== null && typeof v === "object" ? JSON.stringify(v) : `${typeof v}:${String(v)}`); + +function Tagger(tag) { + stream.Transform.call(this, { objectMode: true }); + this.tag = tag; +} +util.inherits(Tagger, stream.Transform); +Tagger.prototype._transform = function (chunk, enc, cb) { + console.log("tagger enc:", enc); + if (chunk === 3) return cb(); + cb(null, { tag: this.tag, value: chunk }); +}; +Tagger.prototype._flush = function (cb) { + this.push({ tag: this.tag, value: "flushed" }); + cb(); +}; + +function Collector() { + stream.Writable.call(this, { objectMode: true, highWaterMark: 2 }); + this.items = []; +} +util.inherits(Collector, stream.Writable); +Collector.prototype._write = function (chunk, enc, cb) { + this.items.push(show(chunk)); + cb(); +}; + +function viaConstructors(next) { + const t = new Tagger("x"); + const c = new Collector(); + console.log("modes:", t.readableObjectMode, t.writableObjectMode, c.writableObjectMode, c.writableHighWaterMark); + t.pipe(c); + c.on("finish", () => { + console.log("collected:", c.items.join(" ")); + next(); + }); + t.write(1); + t.write("two", () => console.log("write cb")); + t.write(3); + t.end({ four: 4 }); +} + +function viaOptionObjects(next) { + const readableOpts = { objectMode: true, read() {} }; + const r = new stream.Readable(readableOpts); + const transformOpts = { + objectMode: true, + transform(chunk, enc, cb) { + cb(null, Array.isArray(chunk) ? chunk.length : chunk); + }, + }; + const t = new stream.Transform(transformOpts); + const seen = []; + const writableOpts = { + objectMode: true, + write(chunk, enc, cb) { + seen.push(show(chunk) + "/" + enc); + cb(); + }, + }; + const w = new stream.Writable(writableOpts); + stream.pipeline(r, t, w, (err) => { + console.log("option objects:", err === undefined ? "ok" : err, seen.join(" ")); + next(); + }); + r.push([1, 2, 3]); + r.push(false); + r.push({ k: "v" }); + r.push(null); +} + +async function viaFrom() { + const values = [0, "s", undefined, [1], { a: { b: 2 } }, 1.5]; + const out = []; + for await (const v of stream.Readable.from(values)) out.push(show(v)); + console.log("from:", out.join(" ")); + const pt = new stream.PassThrough({ objectMode: true }); + const passAlong = (s, v) => s.write(v); + passAlong(pt, { through: true }); + passAlong(pt, 7); + pt.end(); + pt.on("data", (v) => console.log("passed:", show(v))); + await new Promise((resolve) => pt.on("end", resolve)); +} + +viaConstructors(() => viaOptionObjects(() => viaFrom())); diff --git a/tests/corpus/7243-for-await-close-order.mjs b/tests/corpus/7243-for-await-close-order.mjs new file mode 100644 index 0000000000..b3cc700be7 --- /dev/null +++ b/tests/corpus/7243-for-await-close-order.mjs @@ -0,0 +1,58 @@ +// for await over a stream completes through eos: a readable-only stream +// that will emit 'close' (autoDestroy and emitClose) ends the loop after +// 'close', a duplex after 'end', interleaved with other loops' ticks. +import { PassThrough, Readable } from "node:stream"; + +async function interleaved(objectMode) { + const log = []; + let on = true; + (async () => { + for (let i = 0; on && i < 30; i++) { + log.push("t" + i); + await null; + } + })(); + const nums = objectMode ? new PassThrough({ objectMode: true }) : new PassThrough({ highWaterMark: 4 }); + for (let k = 0; k < 6; k++) nums.write(objectMode ? k : "n" + k); + nums.end(); + nums.on("end", () => log.push("N-END")); + nums.on("close", () => log.push("N-CLOSE")); + nums.on("finish", () => log.push("N-FINISH")); + const fr = Readable.from(["a", "b", "c"]); + fr.on("end", () => log.push("F-END")); + fr.on("close", () => log.push("F-CLOSE")); + const a = (async () => { + for await (const n of nums) log.push("n" + String(n).length); + log.push("n-done"); + })(); + const b = (async () => { + for await (const v of fr) log.push("f" + v); + log.push("f-done"); + })(); + await Promise.all([a, b]); + on = false; + console.log(objectMode ? "object:" : "bytes:", log.join(" ")); +} + +async function closeBeforeDone() { + const log = []; + const r = new Readable({ objectMode: true, read() {} }); + r.on("end", () => log.push("end")); + r.on("close", () => log.push("close")); + r.push({ v: 1 }); + r.push(null); + for await (const v of r) log.push("v" + v.v); + log.push("done " + r.destroyed); + const keep = new Readable({ objectMode: true, autoDestroy: false, read() {} }); + keep.on("end", () => log.push("keep end")); + keep.on("close", () => log.push("keep close")); + keep.push(1); + keep.push(null); + for await (const v of keep) log.push("k" + v); + log.push("keep done " + keep.destroyed); + console.log("close order:", log.join(" ")); +} + +await interleaved(true); +await interleaved(false); +await closeBeforeDone(); diff --git a/tests/corpus/7244-objectmode-subclasses.ts b/tests/corpus/7244-objectmode-subclasses.ts new file mode 100644 index 0000000000..5fcb05a24b --- /dev/null +++ b/tests/corpus/7244-objectmode-subclasses.ts @@ -0,0 +1,79 @@ +// Typed object-mode stream subclasses: Transform, Writable and Readable +// classes that pass objectMode to super(), _transform/_write/_read with +// record chunks, encoding parameters typed string | null | undefined, and +// push() of records and other values. +import { Readable, Transform, Writable } from "node:stream"; +import { pipeline } from "node:stream/promises"; + +interface Order { + id: number; + items: string[]; +} + +interface Summary { + id: number; + count: number; +} + +class Orders extends Readable { + private next = 1; + constructor() { + super({ objectMode: true }); + } + _read(): void { + if (this.next > 3) { + this.push(null); + return; + } + const id = this.next++; + this.push({ id, items: Array.from({ length: id }, (_, i) => `item${i}`) }); + } +} + +class Summarize extends Transform { + constructor() { + super({ objectMode: true }); + } + _transform( + order: Order, + encoding: string | null | undefined, + cb: (err: Error | null, data?: Summary) => void, + ): void { + console.log("summarize", order.id, encoding); + cb(null, { id: order.id, count: order.items.length }); + } +} + +class Report extends Writable { + lines: string[] = []; + constructor() { + super({ objectMode: true, highWaterMark: 1 }); + } + _write(s: Summary, _encoding: string, cb: (err?: Error | null) => void): void { + this.lines.push(`#${s.id}=${s.count}`); + cb(); + } +} + +async function main(): Promise { + const report = new Report(); + console.log("report", report.writableObjectMode, report.writableHighWaterMark); + await pipeline(new Orders(), new Summarize(), report); + console.log("lines", report.lines.join(" ")); + + const direct = new Summarize(); + direct.on("data", (s: Summary) => console.log("direct", s.id, s.count)); + direct.write({ id: 9, items: ["a"] }, "utf8"); + direct.write({ id: 10, items: [] }, () => console.log("written")); + direct.end(); + + const values = new Readable({ objectMode: true, read() {} }); + values.push(1.5); + values.push("text"); + values.push([1, 2]); + values.push(null); + const seen: string[] = []; + for await (const v of values) seen.push(JSON.stringify(v)); + console.log("values", seen.join(" ")); +} +main(); diff --git a/tests/corpus/7245-writable-callback-order.mjs b/tests/corpus/7245-writable-callback-order.mjs new file mode 100644 index 0000000000..454e67fe05 --- /dev/null +++ b/tests/corpus/7245-writable-callback-order.mjs @@ -0,0 +1,71 @@ +// Writable callback and 'finish' timing: buffered writes completing +// inside clearBuffer keep their callback order, a deferred afterWrite +// with a user callback holds 'finish' for its own tick, a _final +// callback schedules 'finish' at once, and cork()/uncork() with +// callbacks. +import { PassThrough, Transform, Writable } from "node:stream"; + +function ticks(log, count) { + let n = 0; + const tick = () => { + log.push("t" + n); + if (++n < count) process.nextTick(tick); + }; + process.nextTick(tick); +} + +async function bufferedCallbacks() { + const log = []; + const nums = new PassThrough({ highWaterMark: 4 }); + for (let k = 0; k < 6; k++) log.push("w" + nums.write("n" + k, () => log.push("cb" + k))); + nums.end(() => log.push("end-cb")); + nums.on("finish", () => log.push("finish")); + nums.on("end", () => log.push("end")); + for await (const chunk of nums) log.push("r" + chunk.length); + log.push("done"); + console.log("buffered:", log.join(" ")); +} + +function finishTiming() { + return new Promise((resolve) => { + const log = []; + ticks(log, 10); + const plain = new Writable({ write(c, e, cb) { log.push("pw" + c); cb(); } }); + plain.write("1", () => log.push("pcb1")); + plain.write("2"); + plain.end("3", () => log.push("pend")); + plain.on("finish", () => log.push("pfinish")); + const withFinal = new Writable({ + write(c, e, cb) { log.push("fw" + c); cb(); }, + final(cb) { log.push("final"); cb(); }, + }); + withFinal.write("1", () => log.push("fcb1")); + withFinal.end(() => log.push("fend")); + withFinal.on("finish", () => log.push("ffinish")); + const corked = new Writable({ write(c, e, cb) { log.push("cw" + c); cb(); } }); + corked.cork(); + corked.write("a", () => log.push("ccb-a")); + corked.write("b", () => log.push("ccb-b")); + process.nextTick(() => { + corked.uncork(); + corked.end(() => log.push("cend")); + }); + corked.on("finish", () => log.push("cfinish")); + const t = new Transform({ + transform(c, e, cb) { cb(null, c); }, + flush(cb) { log.push("flush"); cb(); }, + }); + t.resume(); + t.write("q", () => log.push("tcb")); + t.end(() => log.push("tend")); + t.on("finish", () => log.push("tfinish")); + t.on("end", () => log.push("tread-end")); + setTimeout(() => { + console.log("timing:", log.join(" ")); + resolve(undefined); + }, 10); + }); +} + +await bufferedCallbacks(); +await finishTiming(); diff --git a/tests/harness/corpus-costs/7240-objectmode-streams.ts.json b/tests/harness/corpus-costs/7240-objectmode-streams.ts.json new file mode 100644 index 0000000000..2695df1a4d --- /dev/null +++ b/tests/harness/corpus-costs/7240-objectmode-streams.ts.json @@ -0,0 +1 @@ +{ "plain": 2084, "san": 2980 } diff --git a/tests/harness/corpus-costs/7241-objectmode-streams.mjs.json b/tests/harness/corpus-costs/7241-objectmode-streams.mjs.json new file mode 100644 index 0000000000..bd04ea0492 --- /dev/null +++ b/tests/harness/corpus-costs/7241-objectmode-streams.mjs.json @@ -0,0 +1 @@ +{ "plain": 2190, "san": 3093 } diff --git a/tests/harness/corpus-costs/7242-objectmode-dynamic.cjs.json b/tests/harness/corpus-costs/7242-objectmode-dynamic.cjs.json new file mode 100644 index 0000000000..3bc4d9211e --- /dev/null +++ b/tests/harness/corpus-costs/7242-objectmode-dynamic.cjs.json @@ -0,0 +1 @@ +{ "plain": 2430, "san": 3287 } diff --git a/tests/harness/corpus-costs/7243-for-await-close-order.mjs.json b/tests/harness/corpus-costs/7243-for-await-close-order.mjs.json new file mode 100644 index 0000000000..e5826334d7 --- /dev/null +++ b/tests/harness/corpus-costs/7243-for-await-close-order.mjs.json @@ -0,0 +1 @@ +{ "plain": 2402, "san": 3174 } diff --git a/tests/harness/corpus-costs/7244-objectmode-subclasses.ts.json b/tests/harness/corpus-costs/7244-objectmode-subclasses.ts.json new file mode 100644 index 0000000000..d555a94aba --- /dev/null +++ b/tests/harness/corpus-costs/7244-objectmode-subclasses.ts.json @@ -0,0 +1 @@ +{ "plain": 1917, "san": 2782 } diff --git a/tests/harness/corpus-costs/7245-writable-callback-order.mjs.json b/tests/harness/corpus-costs/7245-writable-callback-order.mjs.json new file mode 100644 index 0000000000..0226cbb352 --- /dev/null +++ b/tests/harness/corpus-costs/7245-writable-callback-order.mjs.json @@ -0,0 +1 @@ +{ "plain": 2144, "san": 2747 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c01b52595f..d7187c50b5 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -105,7 +105,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, + runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From c47a0e90aee3d3a7e75c8b6c1cb16b9b3c570f4b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 04:27:35 -0500 Subject: [PATCH 098/395] Compile JSX with the classic runtime Fixes #889 Co-authored-by: papack <33105506+papack@users.noreply.github.com> --- .oxlintrc.jsonc | 1 + docs/content/docs/limitations.mdx | 2 + .../limitations/language-features/jsx.mdx | 5 + .../compiler/src/frontend/jsx-entities.ts | 258 ++++++ .../compiler/src/frontend/jsx-source.test.ts | 244 +++++ packages/compiler/src/frontend/jsx-source.ts | 856 ++++++++++++++++++ packages/compiler/src/frontend/program.ts | 124 ++- .../src/frontend/ts7/enums.generated.ts | 22 + packages/compiler/src/frontend/ts7/enums.ts | 21 + .../compiler/src/frontend/ts7/program-host.ts | 4 + packages/compiler/src/frontend/tsc-codes.ts | 7 + .../6822-jsx-classic-runtime/main.ts.json | 11 + .../6824-jsx-automatic-runtime/main.ts.json | 12 + .../main.ts.json | 8 + .../jsx-factory-mismatch/main.ts.json | 17 + .../diagnostics/jsx-preserve/main.ts.json | 18 + .../diagnostics/jsx-type-errors/main.ts.json | 26 + scripts/generate-ts7-ast-schema.mjs | 2 + .../6822-jsx-classic-runtime/factory.ts | 7 + tests/corpus/6822-jsx-classic-runtime/html.ts | 45 + .../corpus/6822-jsx-classic-runtime/list.jsx | 22 + tests/corpus/6822-jsx-classic-runtime/main.ts | 15 + .../corpus/6822-jsx-classic-runtime/page.tsx | 40 + .../6822-jsx-classic-runtime/pragma.tsx | 11 + .../6822-jsx-classic-runtime/tsconfig.json | 9 + .../corpus/6824-jsx-automatic-runtime/alt.tsx | 3 + .../alt/jsx-runtime.ts | 17 + .../corpus/6824-jsx-automatic-runtime/main.ts | 10 + .../6824-jsx-automatic-runtime/package.json | 8 + .../6824-jsx-automatic-runtime/page.tsx | 33 + .../6824-jsx-automatic-runtime/render.ts | 15 + .../runtime/index.ts | 12 + .../runtime/jsx-runtime.ts | 33 + .../6824-jsx-automatic-runtime/tsconfig.json | 8 + .../6825-jsx-automatic-development/main.ts | 5 + .../package.json | 6 + .../runtime/jsx-dev-runtime.ts | 31 + .../tsconfig.json | 8 + .../6825-jsx-automatic-development/view.tsx | 21 + .../diagnostics/jsx-factory-mismatch/main.ts | 3 + .../jsx-factory-mismatch/tsconfig.json | 1 + .../diagnostics/jsx-factory-mismatch/view.tsx | 13 + tests/diagnostics/jsx-preserve/main.ts | 3 + tests/diagnostics/jsx-preserve/tsconfig.json | 1 + tests/diagnostics/jsx-preserve/view.tsx | 9 + tests/diagnostics/jsx-type-errors/main.ts | 3 + .../diagnostics/jsx-type-errors/tsconfig.json | 1 + tests/diagnostics/jsx-type-errors/view.tsx | 15 + .../jsx-factory-mismatch/main.ts.txt | 6 + .../__snapshots__/jsx-preserve/main.ts.txt | 8 + .../__snapshots__/jsx-type-errors/main.ts.txt | 13 + .../6822-jsx-classic-runtime/main.ts.json | 1 + .../6824-jsx-automatic-runtime/main.ts.json | 1 + .../main.ts.json | 1 + tests/harness/differential.test.ts | 4 +- tests/harness/jsx-oracle.ts | 120 +++ tests/harness/linux-differential.test.ts | 4 +- tests/harness/llvm-differential.test.ts | 4 +- tests/harness/windows-differential.test.ts | 23 +- 59 files changed, 2216 insertions(+), 15 deletions(-) create mode 100644 docs/content/limitations/language-features/jsx.mdx create mode 100644 packages/compiler/src/frontend/jsx-entities.ts create mode 100644 packages/compiler/src/frontend/jsx-source.test.ts create mode 100644 packages/compiler/src/frontend/jsx-source.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6822-jsx-classic-runtime/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6824-jsx-automatic-runtime/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6825-jsx-automatic-development/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-factory-mismatch/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-preserve/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-type-errors/main.ts.json create mode 100644 tests/corpus/6822-jsx-classic-runtime/factory.ts create mode 100644 tests/corpus/6822-jsx-classic-runtime/html.ts create mode 100644 tests/corpus/6822-jsx-classic-runtime/list.jsx create mode 100644 tests/corpus/6822-jsx-classic-runtime/main.ts create mode 100644 tests/corpus/6822-jsx-classic-runtime/page.tsx create mode 100644 tests/corpus/6822-jsx-classic-runtime/pragma.tsx create mode 100644 tests/corpus/6822-jsx-classic-runtime/tsconfig.json create mode 100644 tests/corpus/6824-jsx-automatic-runtime/alt.tsx create mode 100644 tests/corpus/6824-jsx-automatic-runtime/alt/jsx-runtime.ts create mode 100644 tests/corpus/6824-jsx-automatic-runtime/main.ts create mode 100644 tests/corpus/6824-jsx-automatic-runtime/package.json create mode 100644 tests/corpus/6824-jsx-automatic-runtime/page.tsx create mode 100644 tests/corpus/6824-jsx-automatic-runtime/render.ts create mode 100644 tests/corpus/6824-jsx-automatic-runtime/runtime/index.ts create mode 100644 tests/corpus/6824-jsx-automatic-runtime/runtime/jsx-runtime.ts create mode 100644 tests/corpus/6824-jsx-automatic-runtime/tsconfig.json create mode 100644 tests/corpus/6825-jsx-automatic-development/main.ts create mode 100644 tests/corpus/6825-jsx-automatic-development/package.json create mode 100644 tests/corpus/6825-jsx-automatic-development/runtime/jsx-dev-runtime.ts create mode 100644 tests/corpus/6825-jsx-automatic-development/tsconfig.json create mode 100644 tests/corpus/6825-jsx-automatic-development/view.tsx create mode 100644 tests/diagnostics/jsx-factory-mismatch/main.ts create mode 100644 tests/diagnostics/jsx-factory-mismatch/tsconfig.json create mode 100644 tests/diagnostics/jsx-factory-mismatch/view.tsx create mode 100644 tests/diagnostics/jsx-preserve/main.ts create mode 100644 tests/diagnostics/jsx-preserve/tsconfig.json create mode 100644 tests/diagnostics/jsx-preserve/view.tsx create mode 100644 tests/diagnostics/jsx-type-errors/main.ts create mode 100644 tests/diagnostics/jsx-type-errors/tsconfig.json create mode 100644 tests/diagnostics/jsx-type-errors/view.tsx create mode 100644 tests/harness/__snapshots__/jsx-factory-mismatch/main.ts.txt create mode 100644 tests/harness/__snapshots__/jsx-preserve/main.ts.txt create mode 100644 tests/harness/__snapshots__/jsx-type-errors/main.ts.txt create mode 100644 tests/harness/corpus-costs/6822-jsx-classic-runtime/main.ts.json create mode 100644 tests/harness/corpus-costs/6824-jsx-automatic-runtime/main.ts.json create mode 100644 tests/harness/corpus-costs/6825-jsx-automatic-development/main.ts.json create mode 100644 tests/harness/jsx-oracle.ts diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc index 0c5cad25f6..b0acea28a5 100644 --- a/.oxlintrc.jsonc +++ b/.oxlintrc.jsonc @@ -241,6 +241,7 @@ "packages/compiler/src/frontend/comptime-node.ts", "packages/compiler/src/frontend/ts7/world-check.ts", "packages/compiler/src/frontend/ts7/source-parser.test.ts", + "packages/compiler/src/frontend/jsx-source.test.ts", "packages/compiler/src/frontend/npm-static-rewrite.ts", "packages/compiler/src/frontend/npm-static-bundled-cjs.ts", "packages/compiler/src/library/semantic-source.ts", diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index 5be38a9241..e132f27da3 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -34,6 +34,8 @@ title: "Limitations" ../limitations/language-features/commonjs-require.mdx +../limitations/language-features/jsx.mdx + ### Types and class definitions ../limitations/types-and-class-definitions/bigint.mdx diff --git a/docs/content/limitations/language-features/jsx.mdx b/docs/content/limitations/language-features/jsx.mdx new file mode 100644 index 0000000000..a491dc9958 --- /dev/null +++ b/docs/content/limitations/language-features/jsx.mdx @@ -0,0 +1,5 @@ +- **JSX:** JSX in `.tsx`, `.jsx` and JavaScript files compiles with the project's `jsx` setting: `"react"` with `jsxFactory`, `jsxFragmentFactory` or `reactNamespace`, and `"react-jsx"` or `"react-jsxdev"` with `jsxImportSource`, as well as the `@jsx`, `@jsxFrag`, `@jsxImportSource` and `@jsxRuntime` pragmas. + The checker types JSX with these settings, and each element becomes the call TypeScript emits for it, importing the automatic runtime's functions from `/jsx-runtime` or `jsx-dev-runtime`. + `"jsx": "preserve"` and `"react-native"`, which keep JSX in the output, are refused. + The call an element becomes must type-check: a factory or runtime function whose parameters do not accept what an element passes is reported at the element. + With `"react-jsxdev"`, `jsxDEV` receives `undefined` as `self` where `this` has no declared type (in a function that is not a method), which is the value such a function observes when called without a receiver. diff --git a/packages/compiler/src/frontend/jsx-entities.ts b/packages/compiler/src/frontend/jsx-entities.ts new file mode 100644 index 0000000000..b9b0070dea --- /dev/null +++ b/packages/compiler/src/frontend/jsx-entities.ts @@ -0,0 +1,258 @@ +/** The HTML entity names JSX text and attribute strings decode (HTML 4's + * 253 named character references, the set TypeScript's JSX emit decodes), + * by name. */ +export const JSX_ENTITIES: ReadonlyMap = new Map([ + ["quot", 34], + ["amp", 38], + ["apos", 39], + ["lt", 60], + ["gt", 62], + ["nbsp", 160], + ["iexcl", 161], + ["cent", 162], + ["pound", 163], + ["curren", 164], + ["yen", 165], + ["brvbar", 166], + ["sect", 167], + ["uml", 168], + ["copy", 169], + ["ordf", 170], + ["laquo", 171], + ["not", 172], + ["shy", 173], + ["reg", 174], + ["macr", 175], + ["deg", 176], + ["plusmn", 177], + ["sup2", 178], + ["sup3", 179], + ["acute", 180], + ["micro", 181], + ["para", 182], + ["middot", 183], + ["cedil", 184], + ["sup1", 185], + ["ordm", 186], + ["raquo", 187], + ["frac14", 188], + ["frac12", 189], + ["frac34", 190], + ["iquest", 191], + ["Agrave", 192], + ["Aacute", 193], + ["Acirc", 194], + ["Atilde", 195], + ["Auml", 196], + ["Aring", 197], + ["AElig", 198], + ["Ccedil", 199], + ["Egrave", 200], + ["Eacute", 201], + ["Ecirc", 202], + ["Euml", 203], + ["Igrave", 204], + ["Iacute", 205], + ["Icirc", 206], + ["Iuml", 207], + ["ETH", 208], + ["Ntilde", 209], + ["Ograve", 210], + ["Oacute", 211], + ["Ocirc", 212], + ["Otilde", 213], + ["Ouml", 214], + ["times", 215], + ["Oslash", 216], + ["Ugrave", 217], + ["Uacute", 218], + ["Ucirc", 219], + ["Uuml", 220], + ["Yacute", 221], + ["THORN", 222], + ["szlig", 223], + ["agrave", 224], + ["aacute", 225], + ["acirc", 226], + ["atilde", 227], + ["auml", 228], + ["aring", 229], + ["aelig", 230], + ["ccedil", 231], + ["egrave", 232], + ["eacute", 233], + ["ecirc", 234], + ["euml", 235], + ["igrave", 236], + ["iacute", 237], + ["icirc", 238], + ["iuml", 239], + ["eth", 240], + ["ntilde", 241], + ["ograve", 242], + ["oacute", 243], + ["ocirc", 244], + ["otilde", 245], + ["ouml", 246], + ["divide", 247], + ["oslash", 248], + ["ugrave", 249], + ["uacute", 250], + ["ucirc", 251], + ["uuml", 252], + ["yacute", 253], + ["thorn", 254], + ["yuml", 255], + ["OElig", 338], + ["oelig", 339], + ["Scaron", 352], + ["scaron", 353], + ["Yuml", 376], + ["fnof", 402], + ["circ", 710], + ["tilde", 732], + ["Alpha", 913], + ["Beta", 914], + ["Gamma", 915], + ["Delta", 916], + ["Epsilon", 917], + ["Zeta", 918], + ["Eta", 919], + ["Theta", 920], + ["Iota", 921], + ["Kappa", 922], + ["Lambda", 923], + ["Mu", 924], + ["Nu", 925], + ["Xi", 926], + ["Omicron", 927], + ["Pi", 928], + ["Rho", 929], + ["Sigma", 931], + ["Tau", 932], + ["Upsilon", 933], + ["Phi", 934], + ["Chi", 935], + ["Psi", 936], + ["Omega", 937], + ["alpha", 945], + ["beta", 946], + ["gamma", 947], + ["delta", 948], + ["epsilon", 949], + ["zeta", 950], + ["eta", 951], + ["theta", 952], + ["iota", 953], + ["kappa", 954], + ["lambda", 955], + ["mu", 956], + ["nu", 957], + ["xi", 958], + ["omicron", 959], + ["pi", 960], + ["rho", 961], + ["sigmaf", 962], + ["sigma", 963], + ["tau", 964], + ["upsilon", 965], + ["phi", 966], + ["chi", 967], + ["psi", 968], + ["omega", 969], + ["thetasym", 977], + ["upsih", 978], + ["piv", 982], + ["ensp", 8194], + ["emsp", 8195], + ["thinsp", 8201], + ["zwnj", 8204], + ["zwj", 8205], + ["lrm", 8206], + ["rlm", 8207], + ["ndash", 8211], + ["mdash", 8212], + ["lsquo", 8216], + ["rsquo", 8217], + ["sbquo", 8218], + ["ldquo", 8220], + ["rdquo", 8221], + ["bdquo", 8222], + ["dagger", 8224], + ["Dagger", 8225], + ["bull", 8226], + ["hellip", 8230], + ["permil", 8240], + ["prime", 8242], + ["Prime", 8243], + ["lsaquo", 8249], + ["rsaquo", 8250], + ["oline", 8254], + ["frasl", 8260], + ["euro", 8364], + ["image", 8465], + ["weierp", 8472], + ["real", 8476], + ["trade", 8482], + ["alefsym", 8501], + ["larr", 8592], + ["uarr", 8593], + ["rarr", 8594], + ["darr", 8595], + ["harr", 8596], + ["crarr", 8629], + ["lArr", 8656], + ["uArr", 8657], + ["rArr", 8658], + ["dArr", 8659], + ["hArr", 8660], + ["forall", 8704], + ["part", 8706], + ["exist", 8707], + ["empty", 8709], + ["nabla", 8711], + ["isin", 8712], + ["notin", 8713], + ["ni", 8715], + ["prod", 8719], + ["sum", 8721], + ["minus", 8722], + ["lowast", 8727], + ["radic", 8730], + ["prop", 8733], + ["infin", 8734], + ["ang", 8736], + ["and", 8743], + ["or", 8744], + ["cap", 8745], + ["cup", 8746], + ["int", 8747], + ["there4", 8756], + ["sim", 8764], + ["cong", 8773], + ["asymp", 8776], + ["ne", 8800], + ["equiv", 8801], + ["le", 8804], + ["ge", 8805], + ["sub", 8834], + ["sup", 8835], + ["nsub", 8836], + ["sube", 8838], + ["supe", 8839], + ["oplus", 8853], + ["otimes", 8855], + ["perp", 8869], + ["sdot", 8901], + ["lceil", 8968], + ["rceil", 8969], + ["lfloor", 8970], + ["rfloor", 8971], + ["lang", 9001], + ["rang", 9002], + ["loz", 9674], + ["spades", 9824], + ["clubs", 9827], + ["hearts", 9829], + ["diams", 9830], +]); diff --git a/packages/compiler/src/frontend/jsx-source.test.ts b/packages/compiler/src/frontend/jsx-source.test.ts new file mode 100644 index 0000000000..564a61d98f --- /dev/null +++ b/packages/compiler/src/frontend/jsx-source.test.ts @@ -0,0 +1,244 @@ +import ts5 from "typescript5"; +import { afterAll, describe, expect, it } from "vitest"; +import { jsxEmitPlan, decodeEntities, jsxPragmas, jsxTextValue, lowerJsx } from "./jsx-source.js"; +import { closeSourceParser, parseSourceFile } from "./ts7/source-parser-node.js"; + +afterAll(() => closeSourceParser()); + +/** Evaluates a program whose last statement is `result = ...`, with `h` and + * `Fragment` recording the calls the JSX makes. */ +function evaluate(code: string): unknown { + const run = new Function( + "h", + "Fragment", + "React", + `"use strict"; let result; ${code}; return result;`, + ) as (...args: unknown[]) => unknown; + const h = (tag: unknown, props: unknown, ...children: unknown[]): unknown => ({ + tag: typeof tag === "function" ? `fn:${tag.name}` : tag === Fragment ? "#fragment" : tag, + props, + children, + }); + const Fragment = { fragment: true }; + return run(h, Fragment, { createElement: h, Fragment }); +} + +/** TypeScript's own classic-runtime emit of the snippet. */ +function typescriptEmit(source: string, factory?: string): string { + return ts5.transpileModule(source, { + fileName: "case.tsx", + compilerOptions: { + target: ts5.ScriptTarget.ES2022, + module: ts5.ModuleKind.ESNext, + jsx: ts5.JsxEmit.React, + ...(factory === undefined ? {} : { jsxFactory: factory, jsxFragmentFactory: "Fragment" }), + }, + }).outputText; +} + +function rewrite(source: string, factory?: string): string { + const sf = parseSourceFile("/case.tsx", source, "tsx"); + const plan = jsxEmitPlan( + { + jsx: "react", + ...(factory === undefined ? {} : { jsxFactory: factory, jsxFragmentFactory: "Fragment" }), + }, + jsxPragmas(source), + ); + return lowerJsx(sf, plan).text; +} + +const cases: Record = { + "attributes and text": `const name = "w"; const p = { id: 1 }; +result =
Hello {name} & A © + second line + {/* comment */} + {name}{" "}bold +
;`, + fragments: `function Foo() {} Foo.Bar = function Bar() {}; +result = <>;`, + namespaced: `result = ;`, + "keys and nested maps": `const p = { id: 1 }; +result =
{[1, 2].map((n) => {n})}
;`, + "white space only": `result = [
,
+
,
a {"b"} c
];`, + "spreads": `const p = { id: 1 }; const kids = [1, 2]; +result = [
,
,
{...kids}
,
,
];`, + "attribute strings": `result =
;`, + "attribute values": `result =
c={{1}} d={[

]} />;`, + "member and this tags": `class X { Tag = function Tag() {}; render() { return ; } } +result = new X().render();`, + "dashed and capitalized names": `function Comp() {} +result = [, ];`, + "nested JSX in expressions": `const items = ["a", "b"]; +result =

    + {items.length > 1 ?
  • {items[0]}
  • : null} + {items.map((item) => +
  • + {item} +
  • )} +
;`, +}; + +describe("lowerJsx", () => { + for (const [name, source] of Object.entries(cases)) { + it(`matches TypeScript's emit: ${name}`, () => { + const lowered = rewrite(source); + expect(evaluate(lowered)).toEqual(evaluate(typescriptEmit(source))); + expect(lowered.split("\n").length).toBe(source.split("\n").length); + }); + } + + it("uses jsxFactory and the pragmas", () => { + const source = `result = <>;`; + expect(evaluate(rewrite(source, "h"))).toEqual(evaluate(typescriptEmit(source, "h"))); + const pragma = `/** @jsx h */\n/** @jsxFrag Fragment */\nresult = <>;`; + expect(rewrite(pragma)).toContain(`h(Fragment, null`); + expect(evaluate(rewrite(pragma))).toEqual(evaluate(typescriptEmit(pragma))); + }); + + it("keeps every byte outside JSX", () => { + const source = `const a = 1; // \nconst b = "
";\nresult = [a, b, ];\n`; + const lowered = rewrite(source); + expect( + lowered.startsWith(`const a = 1; // \nconst b = "
";\nresult = [a, b, `), + ).toBe(true); + }); + + it("maps rewritten positions back to the source", () => { + const source = `const x = 1;\nresult =
{x}
;\nconst y = 2;\n`; + const sf = parseSourceFile("/case.tsx", source, "tsx"); + const lowered = lowerJsx(sf, { runtime: "classic", factory: "h", fragment: "Fragment" }); + const after = lowered.text.indexOf("const y"); + expect(lowered.originalPosition(after)).toBe(source.indexOf("const y")); + const copied = lowered.text.indexOf("{ a: x }") + "{ a: ".length; + expect(lowered.originalPosition(copied)).toBe(source.indexOf("{x}>") + 1); + expect(lowered.originalPosition(lowered.text.indexOf("h("))).toBe(source.indexOf(" + list + .split(",") + .map((item) => { + const [name, local] = item.trim().split(/\s+as\s+/); + return `const ${local} = runtime.${name};`; + }) + .join(" "), + ) + .replace(/(^|\s)export /gm, "$1"); + const record = + (kind: string) => + (...args: unknown[]): unknown => ({ kind, args: args.map(named) }); + const runtime = { + jsx: record("jsx"), + jsxs: record("jsxs"), + jsxDEV: record("jsxDEV"), + createElement: record("createElement"), + Fragment: "#fragment", + }; + const run = new Function("runtime", `"use strict"; let result; ${body}; return result;`) as ( + runtime: unknown, + ) => unknown; + return run(runtime); +} + +/** Functions compare by name: each evaluation declares its own. */ +function named(value: unknown): unknown { + if (typeof value === "function") return `fn:${value.name}`; + if (Array.isArray(value)) return value.map(named); + if (value !== null && typeof value === "object") + return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, named(item)])); + return value; +} + +function typescriptAutomatic(source: string, development: boolean): string { + return ts5.transpileModule(source, { + fileName: "/case.tsx", + compilerOptions: { + target: ts5.ScriptTarget.ES2022, + module: ts5.ModuleKind.ESNext, + jsx: development ? ts5.JsxEmit.ReactJSXDev : ts5.JsxEmit.ReactJSX, + }, + }).outputText; +} + +function rewriteAutomatic(source: string, development: boolean): string { + const sf = parseSourceFile("/case.tsx", source, "tsx"); + const plan = jsxEmitPlan({ jsx: development ? "react-jsxdev" : "react-jsx" }, jsxPragmas(source)); + return lowerJsx(sf, plan).text; +} + +const automaticCases: Record = { + ...cases, + "keys": `const p = { id: 1 }; +result = [
  • x
  • ,
  • ,
  • ,
  • ,
  • ];`, + "children shapes": `const kids = [1, 2]; +result = [

    ,

    {}

    ,

    +

    ,

    one

    ,

    {1}{2}

    ,

    {...kids}

    ,

    , <>, <>];`, + "names in use": `const _jsx = "taken"; const _jsxs = 1; +result = [

    {_jsx}

    ,

    {_jsxs}{_jsx}

    ];`, +}; + +describe("lowerJsx with the automatic runtime", () => { + for (const [name, source] of Object.entries(automaticCases)) { + for (const development of [false, true]) { + it(`matches TypeScript's ${development ? "react-jsxdev" : "react-jsx"} emit: ${name}`, () => { + const lowered = rewriteAutomatic(`export {};\n${source}`, development); + const expected = typescriptAutomatic(`export {};\n${source}`, development); + expect(evaluateAutomatic(lowered)).toEqual(evaluateAutomatic(expected)); + expect(lowered.split("\n").length).toBe(source.split("\n").length + 1); + }); + } + } + + it("imports the runtime functions on the first statement's line", () => { + const lowered = rewriteAutomatic( + `"use strict";\nexport const a = <>;\n`, + false, + ); + expect(lowered).toBe( + `"use strict";import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; \nexport const a = _jsx(_Fragment, { children: _jsx("b", {}, "k") });\n`, + ); + }); + + it("reads @jsxImportSource and @jsxRuntime", () => { + const pragma = `/** @jsxImportSource preact */\nexport const a = ;\n`; + expect(rewriteAutomatic(pragma, false)).toContain(`from "preact/jsx-runtime"`); + const classic = `/** @jsxRuntime classic */\n/** @jsx h */\nexport const a = ;\n`; + expect(rewriteAutomatic(classic, false)).toContain(`h("b", null)`); + }); +}); + +describe("JSX text", () => { + it("trims lines as TypeScript does", () => { + expect(jsxTextValue(" a \n b \n ")).toBe(" a b"); + expect(jsxTextValue("\n \n")).toBeUndefined(); + expect(jsxTextValue(" ")).toBe(" "); + }); + + it("decodes entities", () => { + expect(decodeEntities("<>& AB&bogus;�")).toBe( + "<>& AB&bogus;�", + ); + }); +}); + +describe("jsxPragmas", () => { + it("reads leading multi-line comments, first @jsx and last @jsxImportSource", () => { + expect( + jsxPragmas( + "#!/usr/bin/env node\n/** @jsx h\n * @jsx g */\n// @jsxFrag F\n/* @jsxImportSource a */ /* @jsxImportSource b */\nconst x = 1; /** @jsx late */", + ), + ).toEqual({ jsx: "h", jsxImportSource: "b" }); + }); +}); diff --git a/packages/compiler/src/frontend/jsx-source.ts b/packages/compiler/src/frontend/jsx-source.ts new file mode 100644 index 0000000000..915a5d16f9 --- /dev/null +++ b/packages/compiler/src/frontend/jsx-source.ts @@ -0,0 +1,856 @@ +/* JSX lowering at the source level, before IR lowering. + * + * A program that contains JSX is checked as written: the checker types JSX + * with the project's jsx settings and reports the errors tsc reports. Each + * file that contains JSX is then rewritten with every JSX expression + * replaced by the calls TypeScript emits for it, and the rewritten program is + * what the lowering compiles, through the same import, call and object + * literal lowering as ordinary code. + * + * The rewrite copies every byte outside JSX expressions, and copies the + * expressions inside them (attribute values, children, spreads) with their + * own JSX rewritten. It keeps every line break of a JSX expression, between + * the arguments it emits, so a position keeps its line in the rewritten + * text. */ + +import * as ts from "./ts7/adapter.js"; +import { jsxOptionName } from "./ts7/enums.js"; +import { JSX_ENTITIES } from "./jsx-entities.js"; + +/** The project's jsx compiler options, as the tsconfig spells them. */ +export interface JsxOptions { + jsx?: string | number | undefined; + jsxFactory?: string | undefined; + jsxFragmentFactory?: string | undefined; + jsxImportSource?: string | undefined; + reactNamespace?: string | undefined; +} + +/** The tsconfig name of a `jsx` option value (TS7 options carry JsxEmit + * numbers, parsed tsconfigs may carry the name). */ +export function jsxEmitName(value: string | number | undefined): string | undefined { + if (typeof value === "number") return jsxOptionName(value); + return value?.toLowerCase(); +} + +/** The `@jsx`, `@jsxFrag`, `@jsxImportSource` and `@jsxRuntime` pragmas of + * a file's leading comments, as TypeScript reads them: multi-line comments + * only, the first `@jsx` and `@jsxFrag` and the last `@jsxImportSource` and + * `@jsxRuntime`. */ +export interface JsxPragmas { + jsx?: string; + jsxFrag?: string; + jsxImportSource?: string; + jsxRuntime?: string; +} + +export function jsxPragmas(text: string): JsxPragmas { + const pragmas: JsxPragmas = {}; + for (const comment of leadingMultiLineComments(text)) { + const pattern = /@(\S+)(\s+(?:\S.*)?)?$/gm; + for (let match = pattern.exec(comment); match !== null; match = pattern.exec(comment)) { + const argument = match[2]?.trim().split(/\s+/)[0]; + if (!argument) continue; + switch (match[1]!.toLowerCase()) { + case "jsx": + pragmas.jsx ??= argument; + break; + case "jsxfrag": + pragmas.jsxFrag ??= argument; + break; + case "jsximportsource": + pragmas.jsxImportSource = argument; + break; + case "jsxruntime": + pragmas.jsxRuntime = argument; + break; + } + } + } + return pragmas; +} + +/** The `/* ... *\/` comments before a file's first token (after a shebang). */ +function leadingMultiLineComments(text: string): string[] { + const comments: string[] = []; + let i = text.startsWith("#!") ? text.indexOf("\n") : 0; + if (i < 0) return comments; + while (i < text.length) { + const ch = text.charCodeAt(i); + if (isLineBreak(ch) || isWhiteSpaceSingleLine(ch)) { + i++; + } else if (text.startsWith("//", i)) { + while (i < text.length && !isLineBreak(text.charCodeAt(i))) i++; + } else if (text.startsWith("/*", i)) { + const end = text.indexOf("*/", i + 2); + const close = end < 0 ? text.length : end + 2; + comments.push(text.slice(i, close)); + i = close; + } else { + break; + } + } + return comments; +} + +function isLineBreak(ch: number): boolean { + return ch === 10 || ch === 13 || ch === 0x2028 || ch === 0x2029; +} + +function isWhiteSpaceSingleLine(ch: number): boolean { + return ( + ch === 32 || + ch === 9 || + ch === 11 || + ch === 12 || + ch === 160 || + ch === 133 || + ch === 5760 || + (ch >= 8192 && ch <= 8203) || + ch === 8239 || + ch === 8287 || + ch === 12288 || + ch === 65279 + ); +} + +/** How a file's JSX is emitted, after its pragmas. */ +export type JsxEmitPlan = + | { + runtime: "classic"; + /** The factory call (`h`, `React.createElement`). */ + factory: string; + /** The fragment tag (`Fragment`, `React.Fragment`). */ + fragment: string; + } + | { + runtime: "automatic"; + /** The module `jsx-runtime` (or `jsx-dev-runtime`) is under, and that + * a `createElement` fallback imports from (`react`). */ + importSource: string; + /** jsx: react-jsxdev: `jsxDEV` calls with source locations. */ + development: boolean; + }; + +/** The plan of a file: the automatic runtime with jsx: react-jsx or + * react-jsxdev, an import source or an `@jsxRuntime automatic` pragma (an + * `@jsxRuntime classic` pragma overrides all of them), the classic runtime + * otherwise. */ +export function jsxEmitPlan(options: JsxOptions, pragmas: JsxPragmas): JsxEmitPlan { + const mode = jsxEmitName(options.jsx); + const automatic = + pragmas.jsxRuntime !== "classic" && + (mode === "react-jsx" || + mode === "react-jsxdev" || + options.jsxImportSource !== undefined || + pragmas.jsxImportSource !== undefined || + pragmas.jsxRuntime === "automatic"); + if (automatic) { + return { + runtime: "automatic", + importSource: pragmas.jsxImportSource ?? options.jsxImportSource ?? "react", + development: mode === "react-jsxdev", + }; + } + const namespace = options.reactNamespace ?? "React"; + return { + runtime: "classic", + factory: pragmas.jsx ?? options.jsxFactory ?? `${namespace}.createElement`, + fragment: pragmas.jsxFrag ?? options.jsxFragmentFactory ?? `${namespace}.Fragment`, + }; +} + +/** True when the file contains a JSX expression. */ +export function containsJsx(sf: ts.SourceFile): boolean { + let found = false; + const visit = (node: ts.Node): void => { + if (found) return; + if (isJsxExpressionRoot(node)) { + found = true; + return; + } + ts.forEachChild(node, visit); + }; + visit(sf); + return found; +} + +/** The first JSX expression of the file (for diagnostics), or null. */ +export function firstJsx(sf: ts.SourceFile): ts.Node | null { + let found: ts.Node | null = null; + const visit = (node: ts.Node): void => { + if (found !== null) return; + if (isJsxExpressionRoot(node)) { + found = node; + return; + } + ts.forEachChild(node, visit); + }; + visit(sf); + return found; +} + +type JsxRoot = ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment; + +function isJsxExpressionRoot(node: ts.Node): node is JsxRoot { + return ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node); +} + +/** A file's text with its JSX lowered, and the way back from a position of + * that text to the source position it stands for. */ +export interface LoweredJsx { + text: string; + /** A rewritten-text offset as an offset of the original text: copied text + * keeps its offset; emitted call text maps to the JSX it stands for. */ + originalPosition(pos: number): number; + /** An original-text offset in the rewritten text: copied text keeps its + * offset; a position inside replaced JSX maps to the call text emitted + * for the JSX at or before it (its end, for the `end` of a span). */ + rewrittenPosition(pos: number, end: boolean): number; +} + +/** The file's text with its JSX replaced by the calls TypeScript emits for + * the plan, and for the automatic runtime the imports of the runtime + * functions those calls use. */ +export function lowerJsx(sf: ts.SourceFile, plan: JsxEmitPlan): LoweredJsx { + return new JsxRewriter(sf, plan).file(); +} + +/** A run of rewritten text: `copied` text starts at `original`; emitted + * text stands for the source position `original`. */ +interface Segment { + rewritten: number; + original: number; + /** The copied length, 0 for emitted text. */ + copied: number; +} + +class JsxRewriter { + private readonly text: string; + /** Output pieces; `cursor` is the source position whose line breaks the + * output has reached. */ + private readonly pieces: string[] = []; + private readonly segments: Segment[] = []; + private length = 0; + private cursor = 0; + + constructor( + private readonly sf: ts.SourceFile, + private readonly plan: JsxEmitPlan, + ) { + this.text = sf.text; + } + + file(): LoweredJsx { + const insertAt = this.plan.runtime === "automatic" ? this.prologueEnd() : -1; + this.copy(0, this.text.length); + if (insertAt >= 0) this.insertImports(insertAt); + const segments = this.segments; + const total = this.length; + return { + text: this.pieces.join(""), + originalPosition: (pos: number): number => { + let low = 0; + let high = segments.length - 1; + while (low < high) { + const mid = (low + high + 1) >> 1; + if (segments[mid]!.rewritten <= pos) low = mid; + else high = mid - 1; + } + const segment = segments[low]; + if (segment === undefined) return pos; + return segment.copied > 0 + ? segment.original + Math.min(pos - segment.rewritten, segment.copied) + : segment.original; + }, + rewrittenPosition: (pos: number, end: boolean): number => { + // Segments are in source order: the last one starting at or before + // `pos` holds it, or is the emitted text for the JSX holding it. + let low = 0; + let high = segments.length - 1; + while (low < high) { + const mid = (low + high + 1) >> 1; + if (segments[mid]!.original <= pos) low = mid; + else high = mid - 1; + } + const segment = segments[low]; + if (segment === undefined) return pos; + if (segment.copied > 0) + return segment.rewritten + Math.min(pos - segment.original, segment.copied); + return end ? (segments[low + 1]?.rewritten ?? total) : segment.rewritten; + }, + }; + } + + /** Local names of the automatic runtime's functions, by module and + * export, in the order of first use (TypeScript's implicit imports). */ + private readonly imports = new Map>(); + private names: Set | null = null; + private fileNameLocal: string | null = null; + + /** The local name bound to `name` of `module`: `_name`, or `_name_1`, + * `_name_2`, ... past an identifier the file already uses. */ + private importName(name: string, module: string): string { + let names = this.imports.get(module); + if (names === undefined) { + names = new Map(); + this.imports.set(module, names); + } + let local = names.get(name); + if (local === undefined) { + local = this.uniqueName(`_${name}`); + names.set(name, local); + } + return local; + } + + private uniqueName(base: string): string { + if (this.names === null) { + const names = new Set(); + const visit = (node: ts.Node): void => { + if (ts.isIdentifier(node)) names.add(node.text); + ts.forEachChild(node, visit); + }; + visit(this.sf); + this.names = names; + } + let name = base; + for (let i = 1; this.names.has(name); i++) name = `${base}_${i}`; + this.names.add(name); + return name; + } + + /** Where the runtime imports go: after the leading "use strict"-style + * directives, else before the first statement, so the inserted text + * shares that statement's line. */ + private prologueEnd(): number { + const statements = this.sf.statements.nodes; + let end = -1; + for (const statement of statements) { + if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression)) break; + end = statement.end; + } + if (end >= 0) return end; + const first = statements[0]; + return first === undefined ? this.text.length : first.getStart(this.sf); + } + + /** Inserts the imports (or, in a CommonJS file, the requires) of the + * runtime functions the file's JSX used, and react-jsxdev's file name + * constant, at `at`: imports in the reverse order of their modules' first + * use, as TypeScript inserts each after the prologue. */ + private insertImports(at: number): void { + if (this.imports.size === 0 && this.fileNameLocal === null) return; + const esm = ts.isExternalModule(this.sf); + const parts: string[] = []; + for (const [module, names] of [...this.imports].reverse()) { + const specifier = JSON.stringify(module); + if (esm) { + const list = [...names].map(([name, local]) => `${name} as ${local}`).join(", "); + parts.push(`import { ${list} } from ${specifier};`); + } else if (/\.[cm]?jsx?$/.test(this.sf.fileName)) { + const list = [...names].map(([name, local]) => `${name}: ${local}`).join(", "); + parts.push(`const { ${list} } = require(${specifier});`); + } + } + if (this.fileNameLocal !== null) + parts.push(`const ${this.fileNameLocal} = ${JSON.stringify(this.sf.fileName)};`); + if (parts.length === 0) return; + const text = `${parts.join(" ")} `; + let index = 0; + let rewritten = 0; + for (; index < this.segments.length; index++) { + const segment = this.segments[index]!; + if (segment.copied > 0 && segment.original <= at && at <= segment.original + segment.copied) { + rewritten = segment.rewritten + (at - segment.original); + break; + } + } + // Split the copied segment holding `at` and shift what follows. + const holder = this.segments[index]; + const joined = this.pieces.join(""); + this.pieces.length = 0; + this.pieces.push(joined.slice(0, rewritten), text, joined.slice(rewritten)); + const following: Segment[] = [{ rewritten, original: at, copied: 0 }]; + if (holder !== undefined && holder.copied > 0) { + const head = at - holder.original; + const tail = holder.copied - head; + holder.copied = head; + if (tail > 0) + following.push({ rewritten: rewritten + text.length, original: at, copied: tail }); + } + for (let i = index + 1; i < this.segments.length; i++) + this.segments[i]!.rewritten += text.length; + this.segments.splice(index + 1, 0, ...following); + if (holder !== undefined && holder.copied === 0) this.segments.splice(index, 1); + this.length += text.length; + } + + /** Emits source text [start, end) unchanged. */ + private copied(start: number, end: number): void { + if (end <= start) return; + this.segments.push({ rewritten: this.length, original: start, copied: end - start }); + this.pieces.push(this.text.slice(start, end)); + this.length += end - start; + } + + /** Emits call text standing for the source position the cursor reached. */ + private emit(...texts: string[]): void { + for (const text of texts) { + if (text.length === 0) continue; + const last = this.segments[this.segments.length - 1]; + if (last === undefined || last.copied > 0 || last.original !== this.cursor) + this.segments.push({ rewritten: this.length, original: this.cursor, copied: 0 }); + this.pieces.push(text); + this.length += text.length; + } + } + + /** Copies the source range [start, end), with its JSX expressions + * replaced. */ + private copy(start: number, end: number): void { + let at = start; + const visit = (node: ts.Node): void => { + if (node.end <= at || (node !== this.sf && node.getStart(this.sf) >= end)) return; + if (isJsxExpressionRoot(node)) { + const nodeStart = node.getStart(this.sf); + this.copied(at, nodeStart); + if (nodeStart > this.cursor) this.cursor = nodeStart; + this.element(node); + this.sync(node.end); + at = node.end; + return; + } + ts.forEachChild(node, visit); + }; + // The cursor never moves back: a range copied after source that follows + // it (jsx()'s key argument, after the children) emits no line break + // twice. + if (start > this.cursor) this.cursor = start; + visit(this.enclosing(start, end)); + this.copied(at, end); + if (end > this.cursor) this.cursor = end; + } + + /** The smallest node around [start, end) whose children cover it. */ + private enclosing(start: number, end: number): ts.Node { + let node: ts.Node = this.sf; + for (;;) { + let next: ts.Node | undefined; + ts.forEachChild(node, (child) => { + if (next === undefined && child.pos <= start && child.end >= end) next = child; + }); + if (next === undefined || next === node) return node; + node = next; + } + } + + /** Emits the line breaks of the source between the cursor and `pos`. */ + private sync(pos: number): void { + for (let i = this.cursor; i < pos; i++) { + const ch = this.text.charCodeAt(i); + if (ch === 13 && this.text.charCodeAt(i + 1) === 10) { + this.cursor = i; + this.emit("\r\n"); + i++; + } else if (isLineBreak(ch)) { + this.cursor = i; + this.emit(this.text[i]!); + } + } + if (pos > this.cursor) this.cursor = pos; + } + + /** Copies an expression in a JSX expression's place. */ + private expression(node: ts.Node): void { + const start = node.getStart(this.sf); + this.sync(start); + this.copy(start, node.end); + } + + private element(node: JsxRoot): void { + const plan = this.plan; + if (plan.runtime === "classic") { + this.createElement(node, plan.factory, plan.fragment); + return; + } + const opening = ts.isJsxFragment(node) + ? null + : ts.isJsxElement(node) + ? node.openingElement + : node; + if (opening !== null && hasKeyAfterPropsSpread(opening.attributes)) { + // A key after a spread of props is not jsx()'s key argument: the + // element is the import source's createElement call. + this.createElement(node, this.importName("createElement", plan.importSource), ""); + return; + } + this.automaticElement(node, opening, plan.importSource, plan.development); + } + + /** The classic-runtime call: `factory(tag, props or null, ...children)`. */ + private createElement(node: JsxRoot, factory: string, fragment: string): void { + const children = ts.isJsxSelfClosingElement(node) ? [] : node.children.nodes; + if (ts.isJsxFragment(node)) { + this.emit(`${factory}(${fragment}, null`); + } else { + const opening = ts.isJsxElement(node) ? node.openingElement : node; + this.emit(`${factory}(`); + this.tagName(opening.tagName); + this.emit(", "); + this.attributes(opening.attributes.properties.nodes, null); + } + for (const child of children) { + if (ts.isJsxText(child)) { + const value = jsxTextValue(child.text); + if (value === undefined) continue; + this.emit(", "); + this.sync(child.getStart(this.sf)); + this.emit(JSON.stringify(value)); + } else if (ts.isJsxExpression(child)) { + if (child.expression === undefined) continue; + this.emit(", "); + if (child.dotDotDotToken !== undefined) this.emit("..."); + this.expression(child.expression); + } else if (isJsxExpressionRoot(child)) { + this.emit(", "); + this.sync(child.getStart(this.sf)); + this.element(child); + } + this.sync(child.end); + } + this.sync(node.end); + this.emit(")"); + } + + /** The automatic-runtime call: `jsx(tag, props[, key])` with the + * children in props, `jsxs` for more than one child or a spread child, and + * react-jsxdev's `jsxDEV(tag, props, key, isStatic, source, self)`. */ + private automaticElement( + node: JsxRoot, + opening: ts.JsxOpeningElement | ts.JsxSelfClosingElement | null, + importSource: string, + development: boolean, + ): void { + const children: readonly ts.JsxChild[] = ts.isJsxSelfClosingElement(node) + ? [] + : node.children.nodes; + const semantic = children.filter(isSemanticJsxChild); + const first = semantic[0]; + const isStatic = + semantic.length > 1 || + (first !== undefined && ts.isJsxExpression(first) && first.dotDotDotToken !== undefined); + const module = `${importSource}/${development ? "jsx-dev-runtime" : "jsx-runtime"}`; + this.emit(`${this.importName(development ? "jsxDEV" : isStatic ? "jsxs" : "jsx", module)}(`); + // TypeScript's children prop: the one semantic child itself, else an + // array of every child that emits a value. + const single = semantic.length === 1 && !isStatic ? first! : null; + const listed = single === null ? children.filter((child) => jsxChildEmits(child)) : []; + const childrenProp = + children.length > 0 && (single !== null ? jsxChildEmits(single) : listed.length > 0) + ? (): void => { + this.emit("children: "); + if (single !== null) { + this.child(single); + } else { + this.emit("["); + listed.forEach((child, index) => { + if (index > 0) this.emit(", "); + this.child(child); + this.sync(child.end); + }); + this.emit("]"); + } + } + : null; + let key: ts.JsxAttribute | undefined; + if (opening === null) { + this.emit(this.importName("Fragment", module), ", "); + if (childrenProp === null) this.emit("{}"); + else this.attributes([], childrenProp); + } else { + this.tagName(opening.tagName); + this.emit(", "); + const properties = opening.attributes.properties.nodes; + key = properties.find( + (property): property is ts.JsxAttribute => + ts.isJsxAttribute(property) && + ts.isIdentifier(property.name) && + property.name.text === "key", + ); + const attributes = properties.filter((property) => property !== key); + if (attributes.length === 0 && childrenProp === null) this.emit("{}"); + else this.attributes(attributes, childrenProp); + } + if (key !== undefined) { + this.emit(", "); + this.attributeValue(key.initializer); + } + if (development) { + if (key === undefined) this.emit(", void 0"); + const { line, column } = lineAndColumn(this.text, node.pos); + this.fileNameLocal ??= this.uniqueName("_jsxFileName"); + this.emit( + `, ${isStatic}, { fileName: ${this.fileNameLocal}, lineNumber: ${line}, columnNumber: ${column} }, `, + jsxSelf(node), + ); + } + this.sync(node.end); + this.emit(")"); + } + + /** A child's value in the children prop (it emits one: jsxChildEmits). */ + private child(child: ts.JsxChild): void { + if (ts.isJsxText(child)) { + this.sync(child.getStart(this.sf)); + this.emit(JSON.stringify(jsxTextValue(child.text)!)); + } else if (ts.isJsxExpression(child)) { + if (child.dotDotDotToken !== undefined) this.emit("..."); + this.expression(child.expression!); + } else { + this.sync(child.getStart(this.sf)); + this.element(child as JsxRoot); + } + } + + private tagName(tag: ts.JsxTagNameExpression): void { + this.sync(tag.getStart(this.sf)); + if (ts.isIdentifier(tag) && isIntrinsicJsxName(tag.text)) { + this.emit(JSON.stringify(tag.text)); + } else if (ts.isJsxNamespacedName(tag)) { + this.emit(JSON.stringify(`${tag.namespace.text}:${tag.name.text}`)); + } else { + this.emit(this.text.slice(tag.getStart(this.sf), tag.end)); + } + this.cursor = tag.end; + } + + /** The props object of `properties`, then `children` (the automatic + * runtime's children prop), or `null` for no properties and no children + * prop. */ + private attributes( + properties: readonly ts.JsxAttributeLike[], + children: (() => void) | null, + ): void { + if (properties.length === 0 && children === null) { + this.emit("null"); + return; + } + if (properties.length === 0) { + this.emit("{ "); + children!(); + this.emit(" }"); + return; + } + this.emit("{ "); + let first = true; + const separate = (): void => { + if (!first) this.emit(", "); + first = false; + }; + for (const attribute of properties) { + if (ts.isJsxSpreadAttribute(attribute)) { + const value = attribute.expression; + if (ts.isObjectLiteralExpression(value) && !hasProtoProperty(value)) { + // TypeScript inlines the properties of a spread object literal. + for (const property of value.properties.nodes) { + separate(); + this.expression(property); + } + } else { + separate(); + this.emit("..."); + this.expression(value); + } + } else { + separate(); + this.sync(attribute.getStart(this.sf)); + this.emit(attributeName(attribute.name), ": "); + this.attributeValue(attribute.initializer); + } + this.sync(attribute.end); + } + if (children !== null) { + this.emit(", "); + children(); + } + this.emit(" }"); + } + + private attributeValue(value: ts.JsxAttributeValue | undefined): void { + if (value === undefined) { + this.emit("true"); + } else if (ts.isStringLiteral(value)) { + this.sync(value.getStart(this.sf)); + this.emit(JSON.stringify(decodeEntities(value.text))); + } else if (ts.isJsxExpression(value)) { + if (value.expression === undefined) this.emit("true"); + else this.expression(value.expression); + } else { + this.sync(value.getStart(this.sf)); + this.element(value as JsxRoot); + } + } +} + +/** A child TypeScript counts: an expression container with an expression, + * text that is not only white space spanning lines, or an element. */ +function isSemanticJsxChild(child: ts.JsxChild): boolean { + if (ts.isJsxExpression(child)) return child.expression !== undefined; + if (ts.isJsxText(child)) return !child.containsOnlyTriviaWhiteSpaces; + return true; +} + +/** True when the child emits a value (text that trims to nothing and empty + * expression containers emit none). */ +function jsxChildEmits(child: ts.JsxChild): boolean { + if (ts.isJsxText(child)) return jsxTextValue(child.text) !== undefined; + if (ts.isJsxExpression(child)) return child.expression !== undefined; + return true; +} + +/** A `key` attribute after a spread that is not an object literal free of + * spreads: TypeScript emits createElement for such an element. */ +function hasKeyAfterPropsSpread(attributes: ts.JsxAttributes): boolean { + let spread = false; + for (const property of attributes.properties.nodes) { + if ( + ts.isJsxSpreadAttribute(property) && + (!ts.isObjectLiteralExpression(property.expression) || + property.expression.properties.nodes.some((p: ts.ObjectLiteralElementLike) => + ts.isSpreadAssignment(p), + )) + ) { + spread = true; + } else if ( + spread && + ts.isJsxAttribute(property) && + ts.isIdentifier(property.name) && + property.name.text === "key" + ) { + return true; + } + } + return false; +} + +/** The 1-based line and column of `pos`, counting the line breaks + * TypeScript counts (\r\n, \n, \r, U+2028, U+2029) and UTF-16 units. */ +function lineAndColumn(text: string, pos: number): { line: number; column: number } { + let line = 1; + let start = 0; + for (let i = 0; i < pos; i++) { + const ch = text.charCodeAt(i); + if (ch === 13 && text.charCodeAt(i + 1) === 10) continue; + if (isLineBreak(ch)) { + line++; + start = i + 1; + } + } + return { line, column: pos - start + 1 }; +} + +/** react-jsxdev's `self` argument. TypeScript passes `this`; where `this` + * has no declared type (the top level of an ES module and a function that is + * not a method and declares no `this` parameter) the argument is the + * `undefined` such code observes when called without a receiver, since an + * untyped `this` does not type-check under noImplicitThis. */ +function jsxSelf(node: ts.Node): string { + for (let at = node.parent; at !== undefined; at = at.parent) { + if (ts.isArrowFunction(at)) continue; + if ( + ts.isMethodDeclaration(at) || + ts.isConstructorDeclaration(at) || + ts.isGetAccessorDeclaration(at) || + ts.isSetAccessorDeclaration(at) || + ts.isPropertyDeclaration(at) || + ts.isClassStaticBlockDeclaration(at) + ) + return "this"; + if (ts.isFunctionDeclaration(at) || ts.isFunctionExpression(at)) { + const declaresThis = at.parameters.nodes.some( + (parameter: ts.ParameterDeclaration) => + ts.isIdentifier(parameter.name) && parameter.name.text === "this", + ); + const objectMethod = + ts.isFunctionExpression(at) && + at.parent !== undefined && + ts.isPropertyAssignment(at.parent); + return declaresThis || objectMethod ? "this" : "undefined"; + } + if (ts.isSourceFile(at)) return ts.isExternalModule(at) ? "undefined" : "this"; + } + return "undefined"; +} + +/** TypeScript's intrinsic-element test: a lowercase first letter or a dash. */ +function isIntrinsicJsxName(name: string): boolean { + const ch = name.charCodeAt(0); + return (ch >= 97 && ch <= 122) || name.includes("-"); +} + +function attributeName(name: ts.JsxAttributeName): string { + if (ts.isJsxNamespacedName(name)) + return JSON.stringify(`${name.namespace.text}:${name.name.text}`); + return /^[A-Z_]\w*$/i.test(name.text) ? name.text : JSON.stringify(name.text); +} + +function hasProtoProperty(object: ts.ObjectLiteralExpression): boolean { + return object.properties.nodes.some( + (property: ts.ObjectLiteralElementLike) => + ts.isPropertyAssignment(property) && + ((ts.isIdentifier(property.name) && property.name.text === "__proto__") || + (ts.isStringLiteral(property.name) && property.name.text === "__proto__")), + ); +} + +/** A JSX text child's string value: each line trimmed (the first line keeps + * its leading and the last line its trailing white space), lines without + * text dropped, the rest joined with one space and entities decoded. + * Undefined when nothing remains. */ +export function jsxTextValue(text: string): string | undefined { + let value: string | undefined; + let firstNonWhitespace = 0; + let lastNonWhitespace = -1; + const add = (line: string): void => { + const decoded = decodeEntities(line); + value = value === undefined ? decoded : `${value} ${decoded}`; + }; + for (let i = 0; i < text.length; i++) { + const ch = text.charCodeAt(i); + if (isLineBreak(ch)) { + if (firstNonWhitespace !== -1 && lastNonWhitespace !== -1) + add(text.slice(firstNonWhitespace, lastNonWhitespace + 1)); + firstNonWhitespace = -1; + } else if (!isWhiteSpaceSingleLine(ch)) { + lastNonWhitespace = i; + if (firstNonWhitespace === -1) firstNonWhitespace = i; + } + } + if (firstNonWhitespace !== -1) add(text.slice(firstNonWhitespace)); + return value; +} + +/** Decodes `&name;`, `&#ddd;` and `&#xhh;` references. Unknown names and + * numbers past U+10FFFF stay as written. */ +export function decodeEntities(text: string): string { + return text.replace( + /&((#((\d+)|x([\da-fA-F]+)))|(\w+));/g, + ( + match: string, + _all: string | undefined, + _number: string | undefined, + _digits: string | undefined, + decimal: string | undefined, + hex: string | undefined, + word: string | undefined, + ) => { + const code = decimal + ? parseInt(decimal, 10) + : hex + ? parseInt(hex, 16) + : JSX_ENTITIES.get(word!); + return code === undefined || code > 0x10ffff ? match : String.fromCodePoint(code); + }, + ); +} diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index e398e59df7..507d3392d9 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -140,6 +140,15 @@ import { } from "./type-only-imports.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js"; +import { + containsJsx, + firstJsx, + jsxEmitName, + jsxEmitPlan, + jsxPragmas, + lowerJsx, + type LoweredJsx, +} from "./jsx-source.js"; const BASE_OPTIONS: ts.Ts7CompilerOptions = { strict: true, @@ -377,6 +386,18 @@ export interface LoadResult { * checkPreflight. */ startupCrash?: StartupCrash | null; configDiags: ScrDiagnostic[]; + /** The files whose JSX the program lowers (jsx-source.ts), by file name. + * The program holds their rewritten text; positions in diagnostics refer + * to it. */ + jsxSources?: ReadonlyMap; + /** With JSX sources: the program over the original text, whose TypeScript + * errors are the program's, and its project world when it has errors. */ + tscWorld?: { + program: ts.Program; + /** The program's pre-emit diagnostics, computed once. */ + diagnostics: readonly ts.Diagnostic[]; + projectWorld: ts.Program | null; + }; /** How scriptc's type world differs from the project's own tsc setup — * used only to label the TypeScript errors those differences cause. */ typeWorld?: TypeWorldDifferences; @@ -778,6 +799,47 @@ function moduleSpecifierKind7(specifier: ts.StringLiteralLike): "import" | "requ : "import"; } +/** The program's files with JSX, lowered with the project's jsx settings + * and each file's pragmas, or the refusal of a setting that emits no calls. + * TypeScript files (.ts, .mts, .cts) cannot hold JSX; without a jsx setting + * the checker already rejects any JSX (TS17004). */ +function lowerProgramJsx7( + program: ts.Program, + options: ts.Ts7CompilerOptions, +): { sources: Map; refusal: ScrDiagnostic | null } { + const sources = new Map(); + const mode = jsxEmitName(options.jsx as string | number | undefined); + if (mode === undefined) return { sources, refusal: null }; + for (const sf of program.getImplementationSourceFiles()) { + if (sf.isDeclarationFile || /\.[mc]?ts$/.test(sf.fileName) || !containsJsx(sf)) continue; + const at = firstJsx(sf)!; + const loc = { file: sf.fileName, start: ts.tokenStartOfNode(at), end: at.end }; + if (mode === "preserve" || mode === "react-native") { + return { + sources, + refusal: { + code: "SC1090", + message: `JSX with "jsx": "${mode}" is not supported: that setting keeps JSX in the output instead of lowering it to calls`, + loc, + hint: 'set "jsx" to "react", "react-jsx" or "react-jsxdev" in the project tsconfig', + }, + }; + } + const plan = jsxEmitPlan( + { + jsx: mode, + jsxFactory: options.jsxFactory as string | undefined, + jsxFragmentFactory: options.jsxFragmentFactory as string | undefined, + jsxImportSource: options.jsxImportSource as string | undefined, + reactNamespace: options.reactNamespace as string | undefined, + }, + jsxPragmas(sf.text), + ); + sources.set(sf.fileName, lowerJsx(sf, plan)); + } + return { sources, refusal: null }; +} + function loadProgram7( host: ts.Ts7Host, entryPath: string, @@ -859,6 +921,26 @@ function loadProgram7( }; program = host.createProgram([...programRoots, overridesDtsPath()], options); } + // JSX: the program above checks the files as written; the program the + // lowering compiles holds them with their JSX lowered. + const jsx = lowerProgramJsx7(program, options); + let tscWorld: LoadResult["tscWorld"]; + if (jsx.refusal !== null) { + config.diags.push(jsx.refusal); + } else if (jsx.sources.size > 0) { + const original = program; + // The project world must read the original text too, so it is built + // before the rewritten files are served, and only when it is needed. + const diagnostics = ts.getPreEmitDiagnostics(original); + const errors = diagnostics.some((d) => d.category === ts.DiagnosticCategory.Error); + tscWorld = { + program: original, + diagnostics, + projectWorld: errors ? host.createProgram(programRoots, options) : null, + }; + for (const [file, lowered] of jsx.sources) host.addVirtualFile(file, lowered.text); + program = host.createProgram([...programRoots, overridesDtsPath()], options); + } const entry = program.getSourceFile(entryPath); if (!entry) throw new Error(`could not load ${entryPath}`); const externalTypeSpecifiersByFile = externalTypeFileClosure7(program, externalTypes); @@ -876,10 +958,14 @@ function loadProgram7( }, externalTypes, externalTypeSpecifiersByFile, + ...(tscWorld === undefined ? {} : { jsxSources: jsx.sources }), + ...(tscWorld === undefined ? {} : { tscWorld }), projectWorld: () => (projectWorld ??= host.createProgram(programRoots, options)), disposeAll: () => { try { projectWorld?.dispose(); + tscWorld?.projectWorld?.dispose(); + tscWorld?.program.dispose(); } finally { program.dispose(); } @@ -4665,8 +4751,8 @@ function preflight7(load: LoadResult): { const sf = p.getSourceFile(d.fileName); return sf !== undefined && isCheckedJsSourceFile(sf, projectCheckJs); }; - const errorsOf = (p: ts.Program): ts.Diagnostic[] => { - const all = ts.getPreEmitDiagnostics(p); + const errorsOf = (p: ts.Program): ts.Diagnostic[] => keptErrors(p, ts.getPreEmitDiagnostics(p)); + const keptErrors = (p: ts.Program, all: readonly ts.Diagnostic[]): ts.Diagnostic[] => { // First pass: every comment-side 2300's (file, name) — the partners // the second pass forgives. const commentDup = new Set(); @@ -4706,9 +4792,39 @@ function preflight7(load: LoadResult): { // instead (the SC1100 checked-cast family). Dirty there → the project- // world errors are the ones reported: they are reproducible with the // project's own tsc, which is what "fix type errors first" asks for. - const tscErrors = errorsOf(program); + // With JSX sources, the program over the original text answers the tsc + // gate; its positions move into the rewritten text the program holds. + // Errors only the rewritten text has come from the calls the JSX lowers + // to (a factory whose signature does not accept them). + const tscWorld = load.tscWorld; + const tscErrors = + tscWorld === undefined ? errorsOf(program) : keptErrors(tscWorld.program, tscWorld.diagnostics); if (tscErrors.length > 0) { - for (const d of errorsOf(load.projectWorld())) diags.push(toPassthrough(d)); + const world = + tscWorld === undefined ? load.projectWorld() : (tscWorld.projectWorld ?? tscWorld.program); + for (const d of errorsOf(world)) { + const diag = toPassthrough(d); + const lowered = tscWorld === undefined ? undefined : load.jsxSources?.get(diag.loc.file); + if (lowered !== undefined) { + diag.loc = { + ...diag.loc, + start: lowered.rewrittenPosition(diag.loc.start, false), + end: lowered.rewrittenPosition(diag.loc.end, true), + }; + } + diags.push(diag); + } + } else if (load.jsxSources !== undefined) { + for (const file of load.jsxSources.keys()) { + const sf = program.getSourceFile(file); + if (sf === undefined) continue; + const own = [...program.getSyntacticDiagnostics(sf), ...program.getSemanticDiagnostics(sf)]; + for (const d of keptErrors(program, own)) { + const diag = toPassthrough(d); + diag.message = `${diag.message} (in the call this JSX lowers to)`; + diags.push(diag); + } + } } // Preflight and lowering share this CheckerFacade. Claim executable diff --git a/packages/compiler/src/frontend/ts7/enums.generated.ts b/packages/compiler/src/frontend/ts7/enums.generated.ts index f21134bb65..4551ab428d 100644 --- a/packages/compiler/src/frontend/ts7/enums.generated.ts +++ b/packages/compiler/src/frontend/ts7/enums.generated.ts @@ -1315,3 +1315,25 @@ export enum LanguageVariant { Standard = 0, JSX = 1 } + +export enum JsxEmit { + None = 0, + Preserve = 1, + ReactNative = 2, + React = 3, + ReactJSX = 4, + ReactJSXDev = 5 +} + +/** Numeric lookup with the SDK's aliases and undefined for unknown values. */ +export function jsxEmitName(value: number): string | undefined { + switch (value) { + case 0: return "None"; + case 1: return "Preserve"; + case 2: return "ReactNative"; + case 3: return "React"; + case 4: return "ReactJSX"; + case 5: return "ReactJSXDev"; + default: return undefined; + } +} diff --git a/packages/compiler/src/frontend/ts7/enums.ts b/packages/compiler/src/frontend/ts7/enums.ts index 6eba358f26..844643e806 100644 --- a/packages/compiler/src/frontend/ts7/enums.ts +++ b/packages/compiler/src/frontend/ts7/enums.ts @@ -1,4 +1,25 @@ /** Runtime enums generated from the pinned TypeScript 7 declarations. * Their nominal names and values match the SDK; TypeScript 5 remains a * separate world. No package loader runs on the compiler's native path. */ +import { jsxEmitName } from "./enums.generated.js"; + export * from "./enums.generated.js"; + +/** The tsconfig spelling of a `jsx` option value: "preserve", "react-native", + * "react", "react-jsx" or "react-jsxdev". */ +export function jsxOptionName(value: number): string | undefined { + switch (jsxEmitName(value)) { + case "Preserve": + return "preserve"; + case "ReactNative": + return "react-native"; + case "React": + return "react"; + case "ReactJSX": + return "react-jsx"; + case "ReactJSXDev": + return "react-jsxdev"; + default: + return undefined; + } +} diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index cc867d2087..c87da0bfe5 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -30,6 +30,7 @@ import type { } from "./session.js"; import { CheckerFacade } from "./checker.js"; import { + jsxOptionName, moduleDetectionKindName, moduleKindName, moduleResolutionKindName, @@ -82,6 +83,9 @@ function serializeOptions(options: Ts7CompilerOptions): Record case "moduleDetection": out[key] = moduleDetectionKindName(value as number)?.toLowerCase() ?? value; break; + case "jsx": + out[key] = typeof value === "number" ? (jsxOptionName(value) ?? value) : value; + break; case "lib": // 5.9.3 spells lib entries "lib.es2025.d.ts"; tsconfig wants "es2025". out[key] = (value as string[]).map((lib) => diff --git a/packages/compiler/src/frontend/tsc-codes.ts b/packages/compiler/src/frontend/tsc-codes.ts index fe48c88550..e6c0be7cd3 100644 --- a/packages/compiler/src/frontend/tsc-codes.ts +++ b/packages/compiler/src/frontend/tsc-codes.ts @@ -27,6 +27,13 @@ export const ADOPTED_OPTIONS = [ // SC1012 "not supported yet" beats a raw TS1259 at the same site. "esModuleInterop", "allowSyntheticDefaultImports", + // JSX: the checker types JSX as the project's tsc does, and the JSX + // lowering (jsx-source.ts) emits the calls that setting selects. + "jsx", + "jsxFactory", + "jsxFragmentFactory", + "jsxImportSource", + "reactNamespace", ] as const; export const JS_RELAXED_TSC_CODES: ReadonlySet = new Set([ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6822-jsx-classic-runtime/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6822-jsx-classic-runtime/main.ts.json new file mode 100644 index 0000000000..9d97dc765b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6822-jsx-classic-runtime/main.ts.json @@ -0,0 +1,11 @@ +{ + "order": [ + "/tests/corpus/6822-jsx-classic-runtime/html.ts", + "/tests/corpus/6822-jsx-classic-runtime/list.jsx", + "/tests/corpus/6822-jsx-classic-runtime/page.tsx", + "/tests/corpus/6822-jsx-classic-runtime/factory.ts", + "/tests/corpus/6822-jsx-classic-runtime/pragma.tsx", + "/tests/corpus/6822-jsx-classic-runtime/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6824-jsx-automatic-runtime/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6824-jsx-automatic-runtime/main.ts.json new file mode 100644 index 0000000000..95ba5cd157 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6824-jsx-automatic-runtime/main.ts.json @@ -0,0 +1,12 @@ +{ + "order": [ + "/tests/corpus/6824-jsx-automatic-runtime/runtime/jsx-runtime.ts", + "/tests/corpus/6824-jsx-automatic-runtime/alt/jsx-runtime.ts", + "/tests/corpus/6824-jsx-automatic-runtime/alt.tsx", + "/tests/corpus/6824-jsx-automatic-runtime/runtime/index.ts", + "/tests/corpus/6824-jsx-automatic-runtime/page.tsx", + "/tests/corpus/6824-jsx-automatic-runtime/render.ts", + "/tests/corpus/6824-jsx-automatic-runtime/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6825-jsx-automatic-development/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6825-jsx-automatic-development/main.ts.json new file mode 100644 index 0000000000..649f0bdff6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6825-jsx-automatic-development/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6825-jsx-automatic-development/runtime/jsx-dev-runtime.ts", + "/tests/corpus/6825-jsx-automatic-development/view.tsx", + "/tests/corpus/6825-jsx-automatic-development/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-factory-mismatch/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-factory-mismatch/main.ts.json new file mode 100644 index 0000000000..9552906ddf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-factory-mismatch/main.ts.json @@ -0,0 +1,17 @@ +{ + "order": [ + "/tests/diagnostics/jsx-factory-mismatch/view.tsx", + "/tests/diagnostics/jsx-factory-mismatch/main.ts" + ], + "diags": [ + { + "code": "SC0001", + "message": "Argument of type 'number' is not assignable to parameter of type 'string'. (in the call this JSX lowers to)", + "loc": { + "file": "/tests/diagnostics/jsx-factory-mismatch/view.tsx", + "start": 337, + "end": 342 + } + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-preserve/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-preserve/main.ts.json new file mode 100644 index 0000000000..eb5e492f46 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-preserve/main.ts.json @@ -0,0 +1,18 @@ +{ + "order": [ + "/tests/diagnostics/jsx-preserve/view.tsx", + "/tests/diagnostics/jsx-preserve/main.ts" + ], + "diags": [ + { + "code": "SC1090", + "message": "JSX with \"jsx\": \"preserve\" is not supported: that setting keeps JSX in the output instead of lowering it to calls", + "loc": { + "file": "/tests/diagnostics/jsx-preserve/view.tsx", + "start": 158, + "end": 176 + }, + "hint": "set \"jsx\" to \"react\", \"react-jsx\" or \"react-jsxdev\" in the project tsconfig" + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-type-errors/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-type-errors/main.ts.json new file mode 100644 index 0000000000..62a162ee24 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/jsx-type-errors/main.ts.json @@ -0,0 +1,26 @@ +{ + "order": [ + "/tests/diagnostics/jsx-type-errors/view.tsx", + "/tests/diagnostics/jsx-type-errors/main.ts" + ], + "diags": [ + { + "code": "SC0001", + "message": "Type 'number' is not assignable to type 'string'.", + "loc": { + "file": "/tests/diagnostics/jsx-type-errors/view.tsx", + "start": 488, + "end": 495 + } + }, + { + "code": "SC0001", + "message": "Type 'string' is not assignable to type 'number'.", + "loc": { + "file": "/tests/diagnostics/jsx-type-errors/view.tsx", + "start": 520, + "end": 527 + } + } + ] +} diff --git a/scripts/generate-ts7-ast-schema.mjs b/scripts/generate-ts7-ast-schema.mjs index a1bc801173..07af474de3 100644 --- a/scripts/generate-ts7-ast-schema.mjs +++ b/scripts/generate-ts7-ast-schema.mjs @@ -44,6 +44,7 @@ const enumNames = [ "ModuleDetectionKind", "OuterExpressionKinds", "LanguageVariant", + "JsxEmit", ]; const enumLines = [ "/* eslint-disable @typescript-eslint/no-duplicate-enum-values -- TypeScript aliases are part of the protocol. */", @@ -67,6 +68,7 @@ for (const name of enumNames) { "ModuleKind", "ModuleResolutionKind", "ModuleDetectionKind", + "JsxEmit", ].includes(name) ) { const values = require(join(packageRoot, "dist/enums", `${basename}.js`))[name]; diff --git a/tests/corpus/6822-jsx-classic-runtime/factory.ts b/tests/corpus/6822-jsx-classic-runtime/factory.ts new file mode 100644 index 0000000000..df09c6ddeb --- /dev/null +++ b/tests/corpus/6822-jsx-classic-runtime/factory.ts @@ -0,0 +1,7 @@ +// A React-shaped factory for the pragma file: createElement and Fragment. +export const Fragment = "frag"; + +export function createElement(tag: string, props: object | null, ...children: unknown[]): string { + const inner = children.map((child) => String(child)).join(","); + return `${tag}${props === null ? "" : JSON.stringify(props)}(${inner})`; +} diff --git a/tests/corpus/6822-jsx-classic-runtime/html.ts b/tests/corpus/6822-jsx-classic-runtime/html.ts new file mode 100644 index 0000000000..2e4beb5cc5 --- /dev/null +++ b/tests/corpus/6822-jsx-classic-runtime/html.ts @@ -0,0 +1,45 @@ +// A small server-side JSX runtime: h builds elements, render writes HTML. +export type Leaf = string | number | boolean | null | undefined | Element; +export type Child = Leaf | Leaf[]; +export type Props = { [name: string]: unknown }; +export type Component = (props: Props, children: Child[]) => Element; +export interface Element { + tag: string; + props: Props | null; + children: Child[]; +} + +export const Fragment = "#fragment"; + +export function h(tag: string | Component, props: Props | null, ...children: Child[]): Element { + if (typeof tag === "function") return tag(props ?? {}, children); + return { tag, props, children }; +} + +function escape(text: string): string { + return text.replace(/&/g, "&").replace(/${inner}`; +} + +declare global { + namespace JSX { + type Element = import("./html.js").Element; + interface IntrinsicElements { + [name: string]: object; + } + } +} diff --git a/tests/corpus/6822-jsx-classic-runtime/list.jsx b/tests/corpus/6822-jsx-classic-runtime/list.jsx new file mode 100644 index 0000000000..d5aa3aee19 --- /dev/null +++ b/tests/corpus/6822-jsx-classic-runtime/list.jsx @@ -0,0 +1,22 @@ +/** @jsx tag */ +// JSX in a JavaScript module, with a JavaScript factory that builds strings. +function tag(name, props, ...children) { + const attributes = Object.entries(props ?? {}) + .map(([key, value]) => ` ${key}=${JSON.stringify(value)}`) + .join(""); + return `<${name}${attributes}>${children.flat().join("")}`; +} + +const names = ["a", "b"]; + +export function List() { + return ( +
      + {names.map((name, index) => ( +
    1. + {name.toUpperCase()} +
    2. + ))} +
    + ); +} diff --git a/tests/corpus/6822-jsx-classic-runtime/main.ts b/tests/corpus/6822-jsx-classic-runtime/main.ts new file mode 100644 index 0000000000..4378557a02 --- /dev/null +++ b/tests/corpus/6822-jsx-classic-runtime/main.ts @@ -0,0 +1,15 @@ +// @tsc-jsx +// JSX in .tsx modules with the classic runtime: jsxFactory, jsxFragmentFactory and per-file pragmas. +import { render } from "./html.js"; +import { List } from "./list.js"; +import { Page } from "./page.js"; +import { viaPragma } from "./pragma.js"; + +const items = [ + { id: 1, name: "write ", done: true }, + { id: 2, name: "ship", done: false }, +]; +console.log(render(Page("Today", items))); +console.log(render(Page("Empty", []))); +console.log(viaPragma); +console.log(List()); diff --git a/tests/corpus/6822-jsx-classic-runtime/page.tsx b/tests/corpus/6822-jsx-classic-runtime/page.tsx new file mode 100644 index 0000000000..c3fa6a03c2 --- /dev/null +++ b/tests/corpus/6822-jsx-classic-runtime/page.tsx @@ -0,0 +1,40 @@ +import { Fragment, h, type Child, type Props } from "./html.js"; + +const Ui = { + Badge(props: Props, children: Child[]) { + return {...children}; + }, +}; + +function Row(props: Props) { + const name = typeof props.name === "string" ? props.name : "?"; + const done = props.done === true; + return ( +
  • + ); +} + +export function Page(title: string, items: { id: number; name: string; done: boolean }[]) { + const extra = { lang: "en", dir: "ltr" }; + return ( + <> +

    {title}

    +
      + {items.map((item) => ( + + ))} +
    +

    + {items.filter((item) => item.done).length} of {items.length} done + multi word + badge +

    + + + <>nested {"fragment"} + + ); +} diff --git a/tests/corpus/6822-jsx-classic-runtime/pragma.tsx b/tests/corpus/6822-jsx-classic-runtime/pragma.tsx new file mode 100644 index 0000000000..3aea8c3303 --- /dev/null +++ b/tests/corpus/6822-jsx-classic-runtime/pragma.tsx @@ -0,0 +1,11 @@ +/** @jsx React.createElement */ +/** @jsxFrag React.Fragment */ +// The per-file pragmas select another factory than the tsconfig's. +import * as React from "./factory.js"; + +export const viaPragma = ( +
    + <>{1} + text +
    +); diff --git a/tests/corpus/6822-jsx-classic-runtime/tsconfig.json b/tests/corpus/6822-jsx-classic-runtime/tsconfig.json new file mode 100644 index 0000000000..6ca523cf29 --- /dev/null +++ b/tests/corpus/6822-jsx-classic-runtime/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "strict": true, + "allowJs": true, + "jsx": "react", + "jsxFactory": "h", + "jsxFragmentFactory": "Fragment" + } +} diff --git a/tests/corpus/6824-jsx-automatic-runtime/alt.tsx b/tests/corpus/6824-jsx-automatic-runtime/alt.tsx new file mode 100644 index 0000000000..a76d0ca729 --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/alt.tsx @@ -0,0 +1,3 @@ +/** @jsxImportSource #alt */ +// This file's JSX uses the runtime the pragma names. +export const viaPragma = [one, {1}{2}, <>frag]; diff --git a/tests/corpus/6824-jsx-automatic-runtime/alt/jsx-runtime.ts b/tests/corpus/6824-jsx-automatic-runtime/alt/jsx-runtime.ts new file mode 100644 index 0000000000..89b942ed22 --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/alt/jsx-runtime.ts @@ -0,0 +1,17 @@ +// A second runtime, selected per file with @jsxImportSource. +export const Fragment = "alt-fragment"; + +export function jsx(type: string, props: { children?: unknown }): string { + return `${type}(${JSON.stringify(props.children ?? null)})`; +} + +export function jsxs(type: string, props: { children: unknown[] }): string { + return `${type}[${props.children.map((child) => JSON.stringify(child)).join(",")}]`; +} + +export namespace JSX { + export type Element = string; + export interface IntrinsicElements { + [name: string]: object; + } +} diff --git a/tests/corpus/6824-jsx-automatic-runtime/main.ts b/tests/corpus/6824-jsx-automatic-runtime/main.ts new file mode 100644 index 0000000000..b25a4bc090 --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/main.ts @@ -0,0 +1,10 @@ +// @tsc-jsx +// JSX with the automatic runtime: jsx/jsxs imports from jsxImportSource, keys, and per-file @jsxImportSource. +import { calls } from "#jsx/jsx-runtime"; +import { viaPragma } from "./alt.js"; +import { page } from "./page.js"; +import { render } from "./render.js"; + +console.log(render(page(["ada", "lin"]))); +console.log(calls.join(" ")); +console.log(viaPragma.join(" | ")); diff --git a/tests/corpus/6824-jsx-automatic-runtime/package.json b/tests/corpus/6824-jsx-automatic-runtime/package.json new file mode 100644 index 0000000000..53f44dc61c --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/package.json @@ -0,0 +1,8 @@ +{ + "type": "module", + "imports": { + "#jsx": "./runtime/index.ts", + "#jsx/jsx-runtime": "./runtime/jsx-runtime.ts", + "#alt/jsx-runtime": "./alt/jsx-runtime.ts" + } +} diff --git a/tests/corpus/6824-jsx-automatic-runtime/page.tsx b/tests/corpus/6824-jsx-automatic-runtime/page.tsx new file mode 100644 index 0000000000..fe7fd8c0c1 --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/page.tsx @@ -0,0 +1,33 @@ +import type { Props } from "#jsx/jsx-runtime"; + +function Card(props: Props) { + return ( +
    +

    {typeof props.title === "string" ? props.title : "?"}

    + {props.children} +
    + ); +} + +export function page(names: string[]) { + const extra = { id: "list", role: "list" }; + return ( + <> + +
      + {names.map((name, index) => ( +
    • + {name} +
    • + ))} +
    +
    +

    fallback

    +

    + two {"children"} +

    +
    {...names}
    +
    + + ); +} diff --git a/tests/corpus/6824-jsx-automatic-runtime/render.ts b/tests/corpus/6824-jsx-automatic-runtime/render.ts new file mode 100644 index 0000000000..6f5f1e3c93 --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/render.ts @@ -0,0 +1,15 @@ +// Renders elements to HTML-like text. +import type { Element } from "#jsx/jsx-runtime"; + +export function render(value: unknown): string { + if (value === null || value === undefined || typeof value === "boolean") return ""; + if (typeof value === "string" || typeof value === "number") return String(value); + if (Array.isArray(value)) return value.map(render).join(""); + const element = value as Element; + const { children, ...rest } = element.props; + const inner = render(children); + if (element.type === "#fragment") return inner; + let attributes = element.key === undefined ? "" : ` key="${element.key}"`; + for (const [name, item] of Object.entries(rest)) attributes += ` ${name}=${JSON.stringify(item)}`; + return `<${element.type}${attributes}>${inner}`; +} diff --git a/tests/corpus/6824-jsx-automatic-runtime/runtime/index.ts b/tests/corpus/6824-jsx-automatic-runtime/runtime/index.ts new file mode 100644 index 0000000000..c7809cdc04 --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/runtime/index.ts @@ -0,0 +1,12 @@ +// The import source's createElement: TypeScript calls it for an element +// whose key follows a spread of props. +import { calls, type Element, type Props } from "./jsx-runtime.js"; + +export function createElement(type: string, props: Props | null, ...children: unknown[]): Element { + calls.push(`createElement:${type}`); + const all: Props = { ...props }; + if (children.length > 0) all["children"] = children.length === 1 ? children[0] : children; + const key = all["key"]; + delete all["key"]; + return { type, props: all, key: key === undefined ? undefined : String(key) }; +} diff --git a/tests/corpus/6824-jsx-automatic-runtime/runtime/jsx-runtime.ts b/tests/corpus/6824-jsx-automatic-runtime/runtime/jsx-runtime.ts new file mode 100644 index 0000000000..185aafbd7a --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/runtime/jsx-runtime.ts @@ -0,0 +1,33 @@ +// The automatic runtime: jsx, jsxs and Fragment, with children in props. +export type Props = { [name: string]: unknown }; +export interface Element { + type: string; + props: Props; + key: string | undefined; +} +export type Component = (props: Props) => Element; + +export const Fragment = "#fragment"; +export const calls: string[] = []; + +function element(kind: string, type: string | Component, props: Props, key?: unknown): Element { + const name = typeof type === "function" ? "component" : type; + calls.push(`${kind}:${name}${key === undefined ? "" : `#${String(key)}`}`); + if (typeof type === "function") return type(props); + return { type, props, key: key === undefined ? undefined : String(key) }; +} + +export function jsx(type: string | Component, props: Props, key?: unknown): Element { + return element("jsx", type, props, key); +} + +export function jsxs(type: string | Component, props: Props, key?: unknown): Element { + return element("jsxs", type, props, key); +} + +export namespace JSX { + export type Element = import("./jsx-runtime.js").Element; + export interface IntrinsicElements { + [name: string]: object; + } +} diff --git a/tests/corpus/6824-jsx-automatic-runtime/tsconfig.json b/tests/corpus/6824-jsx-automatic-runtime/tsconfig.json new file mode 100644 index 0000000000..2e5b6aee77 --- /dev/null +++ b/tests/corpus/6824-jsx-automatic-runtime/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "strict": true, + "module": "nodenext", + "jsx": "react-jsx", + "jsxImportSource": "#jsx" + } +} diff --git a/tests/corpus/6825-jsx-automatic-development/main.ts b/tests/corpus/6825-jsx-automatic-development/main.ts new file mode 100644 index 0000000000..9ff046128c --- /dev/null +++ b/tests/corpus/6825-jsx-automatic-development/main.ts @@ -0,0 +1,5 @@ +// @tsc-jsx +// JSX with react-jsxdev: jsxDEV gets the key, static children, the source position and `this`. +import { views } from "./view.js"; + +for (const view of views()) console.log(view); diff --git a/tests/corpus/6825-jsx-automatic-development/package.json b/tests/corpus/6825-jsx-automatic-development/package.json new file mode 100644 index 0000000000..cbd76bd4ff --- /dev/null +++ b/tests/corpus/6825-jsx-automatic-development/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "imports": { + "#dev/jsx-dev-runtime": "./runtime/jsx-dev-runtime.ts" + } +} diff --git a/tests/corpus/6825-jsx-automatic-development/runtime/jsx-dev-runtime.ts b/tests/corpus/6825-jsx-automatic-development/runtime/jsx-dev-runtime.ts new file mode 100644 index 0000000000..f4e3aea4b5 --- /dev/null +++ b/tests/corpus/6825-jsx-automatic-development/runtime/jsx-dev-runtime.ts @@ -0,0 +1,31 @@ +// The development runtime: jsxDEV receives the key, whether the children +// are static, the element's source position and the calling `this`. +export const Fragment = "#fragment"; + +interface Source { + fileName: string; + lineNumber: number; + columnNumber: number; +} + +export function jsxDEV( + type: string | ((props: { [name: string]: unknown }) => string), + props: { [name: string]: unknown }, + key: unknown, + isStatic: boolean, + source: Source, + self: unknown, +): string { + const file = source.fileName.slice(source.fileName.lastIndexOf("/") + 1); + const where = `${file}:${source.lineNumber}:${source.columnNumber}`; + const owner = self === undefined ? "none" : typeof self; + const name = typeof type === "function" ? type(props) : type; + return `${name}{key=${String(key)} static=${isStatic} at=${where} self=${owner}}`; +} + +export namespace JSX { + export type Element = string; + export interface IntrinsicElements { + [name: string]: object; + } +} diff --git a/tests/corpus/6825-jsx-automatic-development/tsconfig.json b/tests/corpus/6825-jsx-automatic-development/tsconfig.json new file mode 100644 index 0000000000..e1e88d2700 --- /dev/null +++ b/tests/corpus/6825-jsx-automatic-development/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "strict": true, + "module": "nodenext", + "jsx": "react-jsxdev", + "jsxImportSource": "#dev" + } +} diff --git a/tests/corpus/6825-jsx-automatic-development/view.tsx b/tests/corpus/6825-jsx-automatic-development/view.tsx new file mode 100644 index 0000000000..05fdc7b320 --- /dev/null +++ b/tests/corpus/6825-jsx-automatic-development/view.tsx @@ -0,0 +1,21 @@ +function Leaf() { + return "leaf"; +} + +export class Panel { + title = "panel"; + render() { + return
    {1}{2}
    ; + } +} + +export function views(): string[] { + return [ +

    one

    , + , + <> + + , + new Panel().render(), + ]; +} diff --git a/tests/diagnostics/jsx-factory-mismatch/main.ts b/tests/diagnostics/jsx-factory-mismatch/main.ts new file mode 100644 index 0000000000..47f9ff4331 --- /dev/null +++ b/tests/diagnostics/jsx-factory-mismatch/main.ts @@ -0,0 +1,3 @@ +// The factory call a JSX element lowers to must type-check: h takes string children. +import { view } from "./view.js"; +console.log(view); diff --git a/tests/diagnostics/jsx-factory-mismatch/tsconfig.json b/tests/diagnostics/jsx-factory-mismatch/tsconfig.json new file mode 100644 index 0000000000..4fcab4d213 --- /dev/null +++ b/tests/diagnostics/jsx-factory-mismatch/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "jsx": "react", "jsxFactory": "h" } } diff --git a/tests/diagnostics/jsx-factory-mismatch/view.tsx b/tests/diagnostics/jsx-factory-mismatch/view.tsx new file mode 100644 index 0000000000..02bc0a95bf --- /dev/null +++ b/tests/diagnostics/jsx-factory-mismatch/view.tsx @@ -0,0 +1,13 @@ +export function h(tag: string, props: object | null, ...children: string[]): string { + return `<${tag}>${children.join("")}`; +} +declare global { + namespace JSX { + type Element = string; + interface IntrinsicElements { + [name: string]: object; + } + } +} +const count = 3; +export const view =

    count: {count}

    ; diff --git a/tests/diagnostics/jsx-preserve/main.ts b/tests/diagnostics/jsx-preserve/main.ts new file mode 100644 index 0000000000..1a5a8a0bcd --- /dev/null +++ b/tests/diagnostics/jsx-preserve/main.ts @@ -0,0 +1,3 @@ +// "jsx": "preserve" emits JSX unchanged, so there is no call to compile. +import { view } from "./view.js"; +console.log(view); diff --git a/tests/diagnostics/jsx-preserve/tsconfig.json b/tests/diagnostics/jsx-preserve/tsconfig.json new file mode 100644 index 0000000000..8bef9f7f45 --- /dev/null +++ b/tests/diagnostics/jsx-preserve/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "jsx": "preserve" } } diff --git a/tests/diagnostics/jsx-preserve/view.tsx b/tests/diagnostics/jsx-preserve/view.tsx new file mode 100644 index 0000000000..48ce381471 --- /dev/null +++ b/tests/diagnostics/jsx-preserve/view.tsx @@ -0,0 +1,9 @@ +declare global { + namespace JSX { + type Element = string; + interface IntrinsicElements { + [name: string]: object; + } + } +} +export const view =

    kept as JSX

    ; diff --git a/tests/diagnostics/jsx-type-errors/main.ts b/tests/diagnostics/jsx-type-errors/main.ts new file mode 100644 index 0000000000..00c4da53b8 --- /dev/null +++ b/tests/diagnostics/jsx-type-errors/main.ts @@ -0,0 +1,3 @@ +// TypeScript errors in JSX are reported with the positions of the file the program compiles. +import { view } from "./view.js"; +console.log(view); diff --git a/tests/diagnostics/jsx-type-errors/tsconfig.json b/tests/diagnostics/jsx-type-errors/tsconfig.json new file mode 100644 index 0000000000..4fcab4d213 --- /dev/null +++ b/tests/diagnostics/jsx-type-errors/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "jsx": "react", "jsxFactory": "h" } } diff --git a/tests/diagnostics/jsx-type-errors/view.tsx b/tests/diagnostics/jsx-type-errors/view.tsx new file mode 100644 index 0000000000..8a9be99ba0 --- /dev/null +++ b/tests/diagnostics/jsx-type-errors/view.tsx @@ -0,0 +1,15 @@ +export function h(tag: string | ((props: { count: number }) => string), props: object | null, ...children: string[]): string { + return typeof tag === "string" ? `<${tag}>${children.join("")}` : tag(props as { count: number }); +} +declare global { + namespace JSX { + type Element = string; + interface IntrinsicElements { + p: { title?: string }; + } + } +} +function Count(props: { count: number }): string { + return String(props.count); +} +export const view =

    text

    ; diff --git a/tests/harness/__snapshots__/jsx-factory-mismatch/main.ts.txt b/tests/harness/__snapshots__/jsx-factory-mismatch/main.ts.txt new file mode 100644 index 0000000000..e0a19b6542 --- /dev/null +++ b/tests/harness/__snapshots__/jsx-factory-mismatch/main.ts.txt @@ -0,0 +1,6 @@ +jsx-factory-mismatch/view.tsx:13:45 - error SC0001: Argument of type 'number' is not assignable to parameter of type 'string'. (in the call this JSX lowers to) + + 12 | const count = 3; + 13 | export const view = h("p", null, "count: ", count); + | ^~~~~ + 14 | \ No newline at end of file diff --git a/tests/harness/__snapshots__/jsx-preserve/main.ts.txt b/tests/harness/__snapshots__/jsx-preserve/main.ts.txt new file mode 100644 index 0000000000..9e0239088d --- /dev/null +++ b/tests/harness/__snapshots__/jsx-preserve/main.ts.txt @@ -0,0 +1,8 @@ +jsx-preserve/view.tsx:9:21 - error SC1090: JSX with "jsx": "preserve" is not supported: that setting keeps JSX in the output instead of lowering it to calls + + 8 | } + 9 | export const view =

    kept as JSX

    ; + | ^~~~~~~~~~~~~~~~~~ + 10 | + + hint: set "jsx" to "react", "react-jsx" or "react-jsxdev" in the project tsconfig \ No newline at end of file diff --git a/tests/harness/__snapshots__/jsx-type-errors/main.ts.txt b/tests/harness/__snapshots__/jsx-type-errors/main.ts.txt new file mode 100644 index 0000000000..c87c719a85 --- /dev/null +++ b/tests/harness/__snapshots__/jsx-type-errors/main.ts.txt @@ -0,0 +1,13 @@ +jsx-type-errors/view.tsx:15:30 - error SC0001: Type 'number' is not assignable to type 'string'. + + 14 | } + 15 | export const view = h("p", { title: 1 }, "text ", h(Count, { count: "two" })); + | ^~~~~~~ + 16 | + +jsx-type-errors/view.tsx:15:62 - error SC0001: Type 'string' is not assignable to type 'number'. + + 14 | } + 15 | export const view = h("p", { title: 1 }, "text ", h(Count, { count: "two" })); + | ^~~~~~~ + 16 | \ No newline at end of file diff --git a/tests/harness/corpus-costs/6822-jsx-classic-runtime/main.ts.json b/tests/harness/corpus-costs/6822-jsx-classic-runtime/main.ts.json new file mode 100644 index 0000000000..dbc0fa7796 --- /dev/null +++ b/tests/harness/corpus-costs/6822-jsx-classic-runtime/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3400, "san": 2900 } diff --git a/tests/harness/corpus-costs/6824-jsx-automatic-runtime/main.ts.json b/tests/harness/corpus-costs/6824-jsx-automatic-runtime/main.ts.json new file mode 100644 index 0000000000..2a685047e0 --- /dev/null +++ b/tests/harness/corpus-costs/6824-jsx-automatic-runtime/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3600, "san": 3000 } diff --git a/tests/harness/corpus-costs/6825-jsx-automatic-development/main.ts.json b/tests/harness/corpus-costs/6825-jsx-automatic-development/main.ts.json new file mode 100644 index 0000000000..79e32d960e --- /dev/null +++ b/tests/harness/corpus-costs/6825-jsx-automatic-development/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 2500 } diff --git a/tests/harness/differential.test.ts b/tests/harness/differential.test.ts index b70e59c86c..905f648ddf 100644 --- a/tests/harness/differential.test.ts +++ b/tests/harness/differential.test.ts @@ -23,6 +23,7 @@ import ts5 from "typescript"; import { compile } from "@scriptc/compiler"; import { corpusSelect, shardSuffix } from "./shard.js"; import { oracleEnvKey } from "./oracle-env.js"; +import { tscJsxOracleEntry, wantsTscJsx } from "./jsx-oracle.js"; const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); @@ -122,6 +123,7 @@ function wantsTscDecorators(file: string): boolean { * test cache — a pure function of the program bytes and the typescript * version (both in the oracle cache key). */ function nodeOracleFile(file: string): string { + if (wantsTscJsx(directiveHead(file))) return tscJsxOracleEntry(file, cacheDir); if (!wantsTscDecorators(file)) return file; const src = readFileSync(file, "utf8"); const out = ts5.transpileModule(src, { @@ -241,7 +243,7 @@ const comptimeShim = pathToFileURL(join(import.meta.dirname, "comptime-shim.mjs" function programInputs(file: string): string[] { if (!/\/main\.(ts|js|mjs|cjs)$/.test(file)) return [file]; return [ - ...ENTRY_EXTS.flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), + ...[...ENTRY_EXTS, "tsx", "jsx"].flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), ...globSync(join(file, "../**/tsconfig.json")), ...globSync(join(file, "../**/package.json")), ].sort(); diff --git a/tests/harness/jsx-oracle.ts b/tests/harness/jsx-oracle.ts new file mode 100644 index 0000000000..ba7a499462 --- /dev/null +++ b/tests/harness/jsx-oracle.ts @@ -0,0 +1,120 @@ +import { createHash } from "node:crypto"; +import { + copyFileSync, + existsSync, + mkdirSync, + readFileSync, + readdirSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { dirname, join, relative } from "node:path"; +import ts5 from "typescript"; + +/** `// @tsc-jsx` in a directory program's main.ts head: Node cannot run + * .tsx or .jsx modules, so the oracle runs TypeScript's own emit of the + * program. Every .ts, .tsx, .js and .jsx file of the directory is transpiled + * (target ES2022, ES modules) with the jsx options of the directory's + * tsconfig.json, .ts, .tsx and .jsx files becoming .js, and package.json import + * and export targets renamed alike; other files are copied. + * The result lives in a directory under the test cache keyed by the program + * bytes and the typescript version. */ +export function wantsTscJsx(head: readonly string[]): boolean { + return head.some((line) => /^\/\/ @tsc-jsx\s*$/.test(line)); +} + +/** The oracle entry (`main.js` of the transpiled program) for `file`. */ +export function tscJsxOracleEntry(file: string, cacheDir: string): string { + const root = dirname(file); + const files = listFiles(root); + const hash = createHash("sha256").update(ts5.version); + for (const path of files) + hash.update("\0").update(relative(root, path)).update("\0").update(readFileSync(path)); + const dir = join(cacheDir, `jsx-oracle-${hash.digest("hex").slice(0, 16)}`); + const entry = join(dir, "main.js"); + if (existsSync(entry)) return entry; + const configPath = join(root, "tsconfig.json"); + const config = existsSync(configPath) + ? ts5.parseJsonConfigFileContent( + ts5.readConfigFile(configPath, ts5.sys.readFile).config ?? {}, + ts5.sys, + root, + ).options + : {}; + const compilerOptions: ts5.CompilerOptions = { + target: ts5.ScriptTarget.ES2022, + module: ts5.ModuleKind.ESNext, + jsx: config.jsx, + jsxFactory: config.jsxFactory, + jsxFragmentFactory: config.jsxFragmentFactory, + jsxImportSource: config.jsxImportSource, + reactNamespace: config.reactNamespace, + }; + // Atomic publish: build beside the final directory, then rename it into + // place; a concurrent suite that wins the rename leaves the same content. + const staging = `${dir}.${process.pid}.tmp`; + rmSync(staging, { recursive: true, force: true }); + for (const path of files) { + const target = join(staging, relative(root, path)); + mkdirSync(dirname(target), { recursive: true }); + if (/\.(?:[cm]?ts|tsx|jsx?|[cm]js)$/.test(path) && !/\.d\.[cm]?ts$/.test(path)) { + const output = ts5.transpileModule(readFileSync(path, "utf8"), { + compilerOptions, + fileName: path, + }).outputText; + writeFileSync( + target + .replace(/\.(?:ts|tsx|jsx)$/, ".js") + .replace(/\.mts$/, ".mjs") + .replace(/\.cts$/, ".cjs"), + output, + ); + } else if (/(^|\/)package\.json$/.test(path)) { + // Import and export targets follow their files: .ts, .tsx and .jsx + // become .js. + const renamed = (value: unknown): unknown => + typeof value === "string" + ? /\.d\.[cm]?ts$/.test(value) + ? value + : value + .replace(/\.(?:ts|tsx|jsx)$/, ".js") + .replace(/\.mts$/, ".mjs") + .replace(/\.cts$/, ".cjs") + : Array.isArray(value) + ? value.map(renamed) + : value !== null && typeof value === "object" + ? Object.fromEntries(Object.entries(value).map(([key, item]) => [key, renamed(item)])) + : value; + writeFileSync( + target, + `${JSON.stringify(renamed(JSON.parse(readFileSync(path, "utf8"))), null, 2)}\n`, + ); + } else { + copyFileSync(path, target); + } + } + if (!existsSync(join(staging, "package.json"))) + writeFileSync(join(staging, "package.json"), '{ "type": "module" }\n'); + try { + renameSync(staging, dir); + } catch { + rmSync(staging, { recursive: true, force: true }); + } + return entry; +} + +function listFiles(root: string): string[] { + const out: string[] = []; + const visit = (dir: string): void => { + for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => + a.name < b.name ? -1 : a.name > b.name ? 1 : 0, + )) { + const path = join(dir, entry.name); + if (entry.isDirectory()) visit(path); + else if (entry.isFile()) out.push(path); + } + }; + visit(root); + return out; +} diff --git a/tests/harness/linux-differential.test.ts b/tests/harness/linux-differential.test.ts index 9730623cde..9da5199d37 100644 --- a/tests/harness/linux-differential.test.ts +++ b/tests/harness/linux-differential.test.ts @@ -9,6 +9,7 @@ import { compile } from "@scriptc/compiler"; import { npmCases } from "./npm-cases.js"; import { normalizeNodeTestOutput } from "./node-test-normalize.js"; import { eventLoopCases, type StdinScript } from "./event-loop-cases.js"; +import { tscJsxOracleEntry, wantsTscJsx } from "./jsx-oracle.js"; const execFileAsync = promisify(execFile); const enabled = process.env["SCRIPTC_LINUX"] === "1"; @@ -55,6 +56,7 @@ function wantsTscDecorators(file: string): boolean { * programs) its deterministic ES2022 downlevel — differential.test.ts's * nodeOracleFile, with the identical transform and cache key. */ function nodeOracleFile(file: string): string { + if (wantsTscJsx(directiveHead(file))) return tscJsxOracleEntry(file, cacheDir); if (!wantsTscDecorators(file)) return file; const src = readFileSync(file, "utf8"); const out = ts5.transpileModule(src, { @@ -212,7 +214,7 @@ async function runLinuxNode(file: string): Promise { function programInputs(file: string): string[] { if (!/\/main\.(ts|js|mjs|cjs)$/.test(file)) return [file]; return [ - ...ENTRY_EXTS.flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), + ...[...ENTRY_EXTS, "tsx", "jsx"].flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), ...globSync(join(file, "../**/tsconfig.json")), ...globSync(join(file, "../**/package.json")), ].sort(); diff --git a/tests/harness/llvm-differential.test.ts b/tests/harness/llvm-differential.test.ts index 792fdb1d26..15b7e7c0e1 100644 --- a/tests/harness/llvm-differential.test.ts +++ b/tests/harness/llvm-differential.test.ts @@ -11,6 +11,7 @@ import { describe, expect, test } from "vitest"; import ts5 from "typescript"; import { compile } from "@scriptc/compiler"; import { corpusSelect, shardSuffix } from "./shard.js"; +import { tscJsxOracleEntry, wantsTscJsx } from "./jsx-oracle.js"; const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); @@ -120,6 +121,7 @@ function wantsTscDecorators(file: string): boolean { } function nodeOracleFile(file: string): string { + if (wantsTscJsx(directiveHead(file))) return tscJsxOracleEntry(file, cacheDir); if (!wantsTscDecorators(file)) return file; const src = readFileSync(file, "utf8"); const out = ts5.transpileModule(src, { @@ -165,7 +167,7 @@ function nodeOracleArgs(file: string): string[] { function programInputs(file: string): string[] { if (!/\/main\.(ts|js|mjs|cjs)$/.test(file)) return [file]; return [ - ...ENTRY_EXTS.flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), + ...[...ENTRY_EXTS, "tsx", "jsx"].flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), ...globSync(join(file, "../**/tsconfig.json")), ...globSync(join(file, "../**/package.json")), ].sort(); diff --git a/tests/harness/windows-differential.test.ts b/tests/harness/windows-differential.test.ts index 3668204206..8d25d9b072 100644 --- a/tests/harness/windows-differential.test.ts +++ b/tests/harness/windows-differential.test.ts @@ -47,11 +47,12 @@ import { writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; -import { basename, join } from "node:path"; +import { basename, dirname, join } from "node:path"; import { promisify } from "node:util"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; import ts5 from "typescript"; import { compile } from "@scriptc/compiler"; +import { tscJsxOracleEntry, wantsTscJsx } from "./jsx-oracle.js"; const execFileAsync = promisify(execFile); const enabled = process.env["SCRIPTC_WIN"] === "1"; @@ -343,11 +344,13 @@ async function scpToLane(paths: string[]): Promise { * type-strips .ts natively (same pinned version as the macOS lanes). */ async function runWindowsNode(file: string): Promise { const name = laneName(file); - const entry = wantsTscDecorators(file) - ? decOracleName(file) - : name === basename(file) - ? name - : `${name}\\${basename(file)}`; + const entry = wantsTscJsx(directiveHead(file)) + ? `${basename(dirname(tscJsxOracleEntry(file, cacheDir)))}\\main.js` + : wantsTscDecorators(file) + ? decOracleName(file) + : name === basename(file) + ? name + : `${name}\\${basename(file)}`; const transform = wantsTransformTypes(file) ? "--experimental-transform-types --disable-warning=ExperimentalWarning " : ""; @@ -366,7 +369,7 @@ async function runWindowsNode(file: string): Promise { function programInputs(file: string): string[] { if (!/\/main\.(ts|js|mjs|cjs)$/.test(file)) return [file]; return [ - ...ENTRY_EXTS.flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), + ...[...ENTRY_EXTS, "tsx", "jsx"].flatMap((ext) => globSync(join(file, `../**/*.${ext}`))), ...globSync(join(file, "../**/tsconfig.json")), ...globSync(join(file, "../**/package.json")), ].sort(); @@ -403,7 +406,11 @@ async function shipProgram(file: string): Promise { const programRoot = /\/main\.(ts|js|mjs|cjs)$/.test(file) ? join(file, "..") : file; // Decorator programs additionally ship the tsc downlevel the oracle // executes (the compiler still eats the real source above). - const oracleExtra = wantsTscDecorators(file) ? [decOraclePath(file)] : []; + const oracleExtra = wantsTscJsx(directiveHead(file)) + ? [dirname(tscJsxOracleEntry(file, cacheDir))] + : wantsTscDecorators(file) + ? [decOraclePath(file)] + : []; await scpToLane([binary, programRoot, ...oracleExtra]); } From cab2db4f96f3205f35326e069a68bfc6df589953 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 04:27:50 -0500 Subject: [PATCH 099/395] Drop the Cipheriv stream fence from the node-types fixture --- tests/fixtures/node-types/fenced.ts | 4 -- .../__snapshots__/node-types-fenced.txt | 41 ++++++++----------- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index dfd07d446f..274d572724 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -93,10 +93,6 @@ void inspectReadableStream; import { callbackify } from "node:util"; const callbackified = callbackify(async (n: number) => n); void callbackified; -/* Cipheriv values support update(), final(), setAAD(), setAutoPadding(), - * getAuthTag(), and setAuthTag(); the stream.Transform members are fenced. */ -const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); -cipherStream.on("data", () => {}); /* ECDH on a curve Node knows but the runtime does not implement, and * computeSecret(view, encoding), which answers a Buffer where @types/node * declares a string. */ diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 2f1f8cb2e0..3198db3660 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -205,47 +205,38 @@ fenced.ts:94:23 - error SC2020: 'util.callbackify into a callback whose error pa hint: Node calls back with a null error on success; type the binding's callback as (err: NodeJS.ErrnoException | null, ...) => void -fenced.ts:99:1 - error SC2020: 'Cipheriv/Decipheriv.on' is part of the standard library types but has no scriptc lowering yet +fenced.ts:100:12 - error SC2020: 'crypto.createECDH with curve 'sect163k1'' is part of the standard library types but has no scriptc lowering yet - 98 | const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); - 99 | cipherStream.on("data", () => {}); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 100 | /* ECDH on a curve Node knows but the runtime does not implement, and - - hint: update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not - -fenced.ts:104:12 - error SC2020: 'crypto.createECDH with curve 'sect163k1'' is part of the standard library types but has no scriptc lowering yet - - 103 | import { createECDH } from "node:crypto"; - 104 | createECDH("sect163k1"); + 99 | import { createECDH } from "node:crypto"; + 100 | createECDH("sect163k1"); | ^~~~~~~~~~~ - 105 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); + 101 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); hint: the supported curves are prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1, and brainpoolP512r1 -fenced.ts:105:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer or view key' is part of the standard library types but has no scriptc lowering yet +fenced.ts:101:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer or view key' is part of the standard library types but has no scriptc lowering yet - 104 | createECDH("sect163k1"); - 105 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); + 100 | createECDH("sect163k1"); + 101 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 106 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole + 102 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole hint: Node reads the second argument as the key's input encoding and answers a Buffer; pass the output encoding third: computeSecret(key, undefined, encoding) -fenced.ts:110:9 - error SC2020: 'crypto.subtle.wrapKey' is part of the standard library types but has no scriptc lowering yet +fenced.ts:106:9 - error SC2020: 'crypto.subtle.wrapKey' is part of the standard library types but has no scriptc lowering yet - 109 | async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - 110 | await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); + 105 | async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + 106 | await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); | ^~~~~~~~~~~~~~~~~~~~~ - 111 | } + 107 | } hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle -fenced.ts:115:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet +fenced.ts:111:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet - 114 | * compile; the other members fence by name. */ - 115 | const cores = navigator.hardwareConcurrency; + 110 | * compile; the other members fence by name. */ + 111 | const cores = navigator.hardwareConcurrency; | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 116 | void cores; + 112 | void cores; hint: of Node's navigator global only typeof navigator and navigator.userAgent compile \ No newline at end of file From b03e965bf99716c4f353053526fa6ab67182ceda Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 04:35:27 -0500 Subject: [PATCH 100/395] Resolve 7058's promises with undefined so both checkers agree --- tests/corpus/7058-pipeline-function-stages.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/corpus/7058-pipeline-function-stages.mjs b/tests/corpus/7058-pipeline-function-stages.mjs index f0a61e7c25..f75abac394 100644 --- a/tests/corpus/7058-pipeline-function-stages.mjs +++ b/tests/corpus/7058-pipeline-function-stages.mjs @@ -60,7 +60,7 @@ await new Promise((resolve) => { }, (err, value) => { console.log("array source:", err ?? null, value); - resolve(); + resolve(undefined); }, ); }); @@ -114,7 +114,7 @@ function ticker(log) { await new Promise((resolve) => pipeline(["a", "b"], new PassThrough(), sink(log), () => { stop(); - resolve(); + resolve(undefined); }), ); console.log("array ticks:", log.join(" ")); @@ -134,7 +134,7 @@ function ticker(log) { sink(log), () => { stop(); - resolve(); + resolve(undefined); }, ), ); From feec4bf4df9734d23aa0c919429e5de42d902802 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 04:44:34 -0500 Subject: [PATCH 101/395] Call typed-array callbacks directly and visit on an integer counter The helpers of the typed-array callback methods (forEach, map, filter, some, every, find, findIndex, findLast, findLastIndex, reduce, reduceRight) called their callback through the closure's code pointer on every element, and one helper served every call with the same signature, so the callback was never inlined. A literal callback now gets a helper of its own, as for the array methods, and the constant-callback census accepts the typed-array helpers, so the helper calls that function directly. The visit is now a loop on its own counter, `for (let i = 0; i < a.length; i++)` or down from the last index, instead of a loop on a counter declared before it, so the counter is an integer and element reads need no index test. reduce and reduceRight without an initial value read their seed at the first visited index and start one past it. Adds reduce, some and map callbacks to the typed-array-kernels workload. --- benchmarks/runtime/README.md | 2 +- .../runtime/workloads/typed-array-kernels.ts | 11 +++ .../backend/llvm/constant-callbacks.test.ts | 23 +++-- .../src/backend/llvm/constant-callbacks.ts | 12 ++- .../lowering/containers/array-callbacks.ts | 2 +- .../src/frontend/lowering/containers/bytes.ts | 82 ++++++++++++---- .../test/bytes-callback-helpers.test.ts | 68 ++++++++++++++ .../6999-typed-array-callback-loops.ts.json | 6 ++ .../corpus/6999-typed-array-callback-loops.ts | 93 +++++++++++++++++++ .../6999-typed-array-callback-loops.ts.json | 1 + 10 files changed, 268 insertions(+), 32 deletions(-) create mode 100644 packages/compiler/test/bytes-callback-helpers.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6999-typed-array-callback-loops.ts.json create mode 100644 tests/corpus/6999-typed-array-callback-loops.ts create mode 100644 tests/harness/corpus-costs/6999-typed-array-callback-loops.ts.json diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index cbf47675ad..34baccf5ba 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -26,7 +26,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds | | `template-render` | cpu | template literals, escaping, `map`/`join` | | `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread | -| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray | +| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, and `reduce`/`some`/`map` callbacks | | `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage | | `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts | | `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains | diff --git a/benchmarks/runtime/workloads/typed-array-kernels.ts b/benchmarks/runtime/workloads/typed-array-kernels.ts index ba68f7241b..d865ec9146 100644 --- a/benchmarks/runtime/workloads/typed-array-kernels.ts +++ b/benchmarks/runtime/workloads/typed-array-kernels.ts @@ -21,6 +21,14 @@ function brighten(src: Uint8ClampedArray, gain: number, out: Uint8ClampedArray): for (let i = 0; i < out.length; i++) out[i] = src[i]! * gain + 8; } +// Callback methods over a whole typed array. +function callbacks(samples: Float32Array, limit: number, data: Uint8Array): number { + const energy = samples.reduce((sum, v) => sum + v * v, 0); + const loud = samples.some((v) => v > limit) ? 1 : 0; + const halved = data.map((v) => v >> 1); + return energy / samples.length + loud + halved[halved.length >> 1]!; +} + function gray(rgba: Uint8Array, out: Uint8Array): void { for (let i = 0; i < out.length; i++) { const j = i * 4; @@ -73,6 +81,7 @@ let byteSum = 0; let intSum = 0; let brightSum = 0; let lumaSum = 0; +let callbackSum = 0; for (let r = 0; r < rounds; r++) { // Change one input element per round so no round repeats the last. const at = (r * 7919) & (n - 1); @@ -88,6 +97,7 @@ for (let r = 0; r < rounds; r++) { addWrap(ia, ib, io); brighten(pixels, 0.75 + (r & 7) / 8, bright); gray(rgba, luma); + if ((r & 3) === 0) callbackSum += callbacks(fo, 140 + (r & 12), masked); const probe = (r * 104729) & (n - 1); f64Sum += d[probe]!; f32Sum += fo[probe]!; @@ -102,3 +112,4 @@ console.log(`uint8 ${byteSum}`); console.log(`int32 ${intSum}`); console.log(`clamped ${brightSum}`); console.log(`gray ${lumaSum}`); +console.log(`callbacks ${callbackSum.toFixed(3)}`); diff --git a/packages/compiler/src/backend/llvm/constant-callbacks.test.ts b/packages/compiler/src/backend/llvm/constant-callbacks.test.ts index aa491c7111..e8ca5571f3 100644 --- a/packages/compiler/src/backend/llvm/constant-callbacks.test.ts +++ b/packages/compiler/src/backend/llvm/constant-callbacks.test.ts @@ -314,16 +314,19 @@ test("virtual calls admit every same-named method", () => { expect(new AmbientReceiverReaders(functions).ignores(caller)).toBe(false); }); -test("synthetic array HOF helpers are constant-callback consumers", () => { - const mod = fixture(); - const consumer = mod.functions[1]!; - const entry = mod.functions[2]!; - delete consumer.ownsPrototype; - consumer.name = "%arr.some.0"; - const call = entry.body[0]!; - if (call.kind === "exprStmt" && call.expr.kind === "call") call.expr.callee = "%arr.some.0"; - expect(facts(mod).get("%arr.some.0")?.get("test")).toBe("predicate"); -}); +test.each(["%arr.some.0", "%bytes.some.0"])( + "synthetic array and typed-array HOF helpers are constant-callback consumers (%s)", + (name) => { + const mod = fixture(); + const consumer = mod.functions[1]!; + const entry = mod.functions[2]!; + delete consumer.ownsPrototype; + consumer.name = name; + const call = entry.body[0]!; + if (call.kind === "exprStmt" && call.expr.kind === "call") call.expr.callee = name; + expect(facts(mod).get(name)?.get("test")).toBe("predicate"); + }, +); function factory(name: string, returns: IrExpr[]): IrFunction { return { diff --git a/packages/compiler/src/backend/llvm/constant-callbacks.ts b/packages/compiler/src/backend/llvm/constant-callbacks.ts index 6e61554600..8a898d6a09 100644 --- a/packages/compiler/src/backend/llvm/constant-callbacks.ts +++ b/packages/compiler/src/backend/llvm/constant-callbacks.ts @@ -29,12 +29,16 @@ export function findConstantCallbacks( mod.functions .filter( (fn) => - // Authored declarations, plus the frontend's synthetic array - // HOF loops (`%arr..`): those are plain direct-call - // functions, and a literal callback gets a helper of its own. + // Authored declarations, plus the frontend's synthetic array and + // typed-array HOF loops (`%arr..`, `%bytes..`): + // those are plain direct-call functions, and a literal callback + // gets a helper of its own. // Generator declarations are entered through their spawn wrapper, // which moves the same arguments into the generator's frame. - (fn.ownsPrototype || fn.name.startsWith("%arr.") || fn.generator !== undefined) && + (fn.ownsPrototype || + fn.name.startsWith("%arr.") || + fn.name.startsWith("%bytes.") || + fn.generator !== undefined) && (!fn.async || fn.generator !== undefined) && !fn.captures && !fn.classCaptures && diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 7ca69d9ba3..612c47ea04 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -452,7 +452,7 @@ function withFastCallback(fn: IrFunction, fastT: CallbackType | null, elem: IrTy * constant-callback census then calls it directly and LLVM can inline the * callback into the loop (TurboFan's builtin+callback inlining). Callback * values keep the shared, type-interned helper. */ -function callbackSite(fnArg: IrExpr): string { +export function callbackSite(fnArg: IrExpr): string { return fnArg.kind === "closure" ? `@${fnArg.fnName}` : ""; } diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 8a43647543..fd4420b698 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -43,7 +43,7 @@ import { lowerArraySpreadItems, lowerCheckedArrayFrom } from "./array-constructi import { lowerToNumberArgument } from "../lower-exprs.js"; import { lowerBytesIteratorValue } from "../iterator-adapters.js"; import { lowerArrayCallback } from "./callback-arguments.js"; -import { findPredicateOk, predicateToBoolean } from "./array-callbacks.js"; +import { callbackSite, findPredicateOk, predicateToBoolean } from "./array-callbacks.js"; import { typeKey } from "../../type-mapper.js"; /** TypedArray.prototype.sort and toSorted. The receiver and comparator @@ -251,13 +251,28 @@ function lowerBytesCallbackCall( : method === "findIndex" || method === "findLastIndex" ? F64 : lowerer.irTypeOf(call)); - const helper = bytesCallbackHelper(lowerer, method, bytesT, resultT, fnRet, arity, hasInit, loc); + const helper = bytesCallbackHelper( + lowerer, + method, + bytesT, + resultT, + fnRet, + arity, + hasInit, + loc, + callbackSite(fnArg), + ); const args: IrExpr[] = [receiver, fnArg]; if (hasInit) args.push(lowerer.lowerExprExpecting(call.arguments.nodes[1]!, resultT)); return { kind: "call", callee: helper, args, type: resultT, loc }; } -/** The interned helper of one typed-array callback method shape. */ +/** The interned helper of one typed-array callback method shape. A literal + * callback gets a helper of its own (`site`), which the backend then calls + * directly, as for the array methods. The visit is a loop on its own + * counter (`for (let i = 0; i < a.length; i++)`, or down from the last + * index), so the counter is an integer and the element reads need no + * index test. */ function bytesCallbackHelper( lowerer: Lowerer, method: BytesCallbackMethod, @@ -267,8 +282,9 @@ function bytesCallbackHelper( arity: number, hasInit: boolean, loc: SrcLoc, + site: string, ): string { - const key = `bytes.${method}:${bytesT.elem}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : ""}`; + const key = `bytes.${method}:${bytesT.elem}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : ""}${site}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; const name = keyedHelperName(lowerer, `%bytes.${method}`, key); @@ -300,10 +316,20 @@ function bytesCallbackHelper( type: fnRet, loc, }); + // The forward bound is the receiver's length itself (fixed for a typed + // array, and equal to `n`), which the emitter's typed-array loops use. + const length = (): IrExpr => ({ + kind: "bytesIntrinsic", + method: "length", + receiver: a(), + args: [], + type: F64, + loc, + }); const inRange = (): IrExpr => backward ? { kind: "bin", op: ">=", left: i(), right: numLit(0, loc), type: BOOL, loc } - : { kind: "bin", op: "<", left: i(), right: n(), type: BOOL, loc }; + : { kind: "bin", op: "<", left: i(), right: length(), type: BOOL, loc }; const advance = (): IrStmt => ({ kind: "assign", localId: "i.0", @@ -344,6 +370,7 @@ function bytesCallbackHelper( ...(hasInit ? [{ localId: "z.0", name: "z", type: resultT }] : []), ]; const visit: IrStmt[] = [{ kind: "assign", localId: "v.0", value: at(i()), loc }]; + let seeded = false; const prologue: IrStmt[] = []; let epilogue: IrStmt[]; switch (method) { @@ -459,10 +486,12 @@ function bytesCallbackHelper( loc, }); } else { + // The seed is the first visited element; the loop starts past it. + seeded = true; prologue.push( { kind: "if", - cond: { kind: "unary", op: "!", operand: inRange(), type: BOOL, loc }, + cond: { kind: "bin", op: "===", left: n(), right: numLit(0, loc), type: BOOL, loc }, then: [ { kind: "throw", @@ -486,8 +515,16 @@ function bytesCallbackHelper( else_: null, loc, }, - { kind: "varDecl", localId: "acc.0", init: at(i()), loc }, - advance(), + { + kind: "varDecl", + localId: "acc.0", + init: at( + backward + ? { kind: "bin", op: "-", left: n(), right: numLit(1, loc), type: F64, loc } + : numLit(0, loc), + ), + loc, + }, ); } visit.push({ @@ -506,17 +543,30 @@ function bytesCallbackHelper( init: { kind: "bytesIntrinsic", method: "length", receiver: a(), args: [], type: F64, loc }, loc, }, + { kind: "varDecl", localId: "v.0", init: numLit(0, loc), loc }, + ...prologue, { - kind: "varDecl", - localId: "i.0", - init: backward - ? { kind: "bin", op: "-", left: n(), right: numLit(1, loc), type: F64, loc } - : numLit(0, loc), + kind: "for", + init: { + kind: "varDecl", + localId: "i.0", + init: backward + ? { + kind: "bin", + op: "-", + left: n(), + right: numLit(seeded ? 2 : 1, loc), + type: F64, + loc, + } + : numLit(seeded ? 1 : 0, loc), + loc, + }, + cond: inRange(), + update: advance(), + body: visit, loc, }, - { kind: "varDecl", localId: "v.0", init: numLit(0, loc), loc }, - ...prologue, - { kind: "for", init: null, cond: inRange(), update: advance(), body: visit, loc }, ...epilogue, ]; lowerer.liftedFns.push({ name, params, returnType: resultT, locals, body, loc }); diff --git a/packages/compiler/test/bytes-callback-helpers.test.ts b/packages/compiler/test/bytes-callback-helpers.test.ts new file mode 100644 index 0000000000..c47d106c48 --- /dev/null +++ b/packages/compiler/test/bytes-callback-helpers.test.ts @@ -0,0 +1,68 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule, validateModule } from "../src/index.js"; +import type { IrFunction, IrModule, IrStmt } from "../src/ir/ir.js"; + +async function lower(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-bytes-callbacks-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const mod = deserializeModule(await readFile(outPath, "utf8")); + expect(validateModule(mod)).toEqual([]); + return mod; + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const helpers = (mod: IrModule, method: string): IrFunction[] => + mod.functions.filter((f) => f.name.startsWith(`%bytes.${method}.`)); + +const visitLoop = (fn: IrFunction): IrStmt & { kind: "for" } => { + const loop = fn.body.find((s): s is IrStmt & { kind: "for" } => s.kind === "for"); + if (!loop) throw new Error(`no loop in ${fn.name}`); + return loop; +}; + +test("literal callbacks get helpers of their own; function values share one", async () => { + const mod = await lower(` + const a = new Int32Array([1, 2, 3]); + const double = (v: number): number => v * 2; + console.log(a.map((v) => v + 1).join(","), a.map((v) => v - 1).join(",")); + console.log(a.map(double).join(","), new Int32Array([4]).map(double).join(",")); + `); + expect(helpers(mod, "map")).toHaveLength(3); +}); + +test("typed-array callback loops run on their own counter", async () => { + const mod = await lower(` + const a = new Float64Array([1.5, 2.5, 3.5]); + console.log(a.some((v) => v > 2), a.reduce((s, v) => s + v)); + console.log(a.reduceRight((s, v) => s - v), a.findLastIndex((v) => v < 3)); + `); + for (const method of ["some", "reduce"]) { + const loop = visitLoop(helpers(mod, method)[0]!); + // for (let i = <0 or 1 past reduce's seed>; i < a.length; i = i + 1) + expect(loop.init).toMatchObject({ kind: "varDecl", localId: "i.0", init: { kind: "numLit" } }); + expect(loop.cond).toMatchObject({ + kind: "bin", + op: "<", + right: { kind: "bytesIntrinsic", method: "length" }, + }); + } + expect(visitLoop(helpers(mod, "reduce")[0]!).init).toMatchObject({ init: { value: 1 } }); + // Backward: from n - 2 past reduceRight's seed, and from n - 1. + expect(visitLoop(helpers(mod, "reduceRight")[0]!).init).toMatchObject({ + init: { kind: "bin", op: "-", right: { value: 2 } }, + }); + expect(visitLoop(helpers(mod, "findLastIndex")[0]!).init).toMatchObject({ + init: { kind: "bin", op: "-", right: { value: 1 } }, + }); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6999-typed-array-callback-loops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6999-typed-array-callback-loops.ts.json new file mode 100644 index 0000000000..de7ab0a69b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6999-typed-array-callback-loops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6999-typed-array-callback-loops.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6999-typed-array-callback-loops.ts b/tests/corpus/6999-typed-array-callback-loops.ts new file mode 100644 index 0000000000..84adfd9952 --- /dev/null +++ b/tests/corpus/6999-typed-array-callback-loops.ts @@ -0,0 +1,93 @@ +// Typed-array callback methods with literal callbacks, function values shared +// by several calls, and callbacks chosen at run time: visit order and +// arguments, writes during the visit, early exits, reduce seeds and the +// empty-array TypeError, map conversions, Buffer results, throwing +// callbacks, empty arrays and subarray receivers. + +const ints = new Int32Array([5, -3, 8, 0, 12, -7, 4]); +const floats = new Float32Array([0.1, 2.5, -1.25, 3.75, 9.5]); + +const visits: string[] = []; +ints.forEach((v, i, a) => { + visits.push(`${i}:${v}:${a === ints}`); +}); +console.log("forEach", visits.join(" ")); + +const writes = new Uint8Array([1, 2, 3, 4, 5, 6]); +writes.forEach((v, i, a) => { + if (i + 1 < a.length) a[i + 1] = a[i + 1]! + v; +}); +console.log("forEach writes ahead", writes.join(",")); + +let calls = 0; +const counted = (v: number): boolean => { + calls++; + return v > 7; +}; +console.log("some", ints.some(counted), calls); +calls = 0; +console.log("every", ints.every((v) => (calls++, v < 10)), calls); +calls = 0; +console.log("find", ints.find(counted), calls, ints.find((v) => v > 100)); +calls = 0; +console.log("findIndex", ints.findIndex(counted), calls, ints.findIndex((v) => v > 100)); + +const order: number[] = []; +console.log( + "findLast", + ints.findLast((v, i) => (order.push(i), v < 0)), + ints.findLastIndex((v) => v > 100), + order.join(","), +); + +console.log("reduce", ints.reduce((acc, v) => acc + v), ints.reduce((acc, v, i) => acc + v * i, 100)); +const right: number[] = []; +console.log( + "reduceRight", + floats.reduceRight((acc, v, i) => (right.push(i), acc - v)), + right.join(","), + floats.reduceRight((acc, v) => acc + v, -1), +); +console.log("single", new Int8Array([42]).reduce(() => -1), new Int8Array([42]).reduceRight(() => -1)); +function tryReduce(label: string, run: () => number): void { + try { + console.log(label, run()); + } catch (error) { + console.log(label, (error as Error).name, (error as Error).message); + } +} +const emptyU16 = new Uint16Array(0); +const emptyF64 = new Float64Array(0); +tryReduce("empty u16 reduce", () => emptyU16.reduce((a, b) => a + b)); +tryReduce("empty f64 reduceRight", () => emptyF64.reduceRight((a, b) => a + b)); +tryReduce("empty with init", () => emptyU16.reduce((a, b) => a + b, 7) + emptyF64.reduceRight((a, b) => a + b, 1)); +console.log("empty", emptyU16.some(() => true), emptyF64.every(() => false), emptyU16.map((v) => v).length); + +console.log("map u8", new Uint8Array([250, 3, 128]).map((v) => v * 3).join(",")); +console.log("map clamped", new Uint8ClampedArray([250, 3, 128]).map((v) => v * 1.5 - 10).join(",")); +console.log("map i16", new Int16Array([30000, -30000, 7]).map((v, i) => v + i * 10000).join(",")); +console.log("map f32", floats.map((v) => v / 3).join(",")); +console.log("filter", ints.filter((v, i) => v > 0 && i % 2 === 0).join(","), floats.filter((v) => v > 1).join(",")); + +const buf = Buffer.from("scriptc"); +const upper = buf.map((c) => c - 32); +console.log("buffer map", Buffer.isBuffer(upper), upper.join(","), Buffer.isBuffer(buf.filter((c) => c > 112))); + +// One function value at several calls, and callbacks chosen at run time. +const double = (v: number): number => v * 2; +console.log("shared", new Int32Array([1, 2, 3]).map(double).join(","), new Int32Array([4, 5]).map(double).join(",")); +const pick = (k: number): ((v: number) => boolean) => (k > 0 ? (v) => v > k : (v) => v < k); +console.log("chosen", ints.filter(pick(4)).join(","), ints.filter(pick(-4)).join(",")); + +try { + let seen = 0; + floats.forEach((v) => { + seen++; + if (v < 0) throw new RangeError(`negative ${v} after ${seen}`); + }); +} catch (error) { + console.log("throw", (error as Error).name, (error as Error).message); +} + +const view = ints.subarray(2, 5); +console.log("subarray", view.map((v) => v + 1).join(","), view.reduce((a, v) => a * 10 + v, 0), view.findLastIndex((v) => v === 8)); diff --git a/tests/harness/corpus-costs/6999-typed-array-callback-loops.ts.json b/tests/harness/corpus-costs/6999-typed-array-callback-loops.ts.json new file mode 100644 index 0000000000..63d9448b4e --- /dev/null +++ b/tests/harness/corpus-costs/6999-typed-array-callback-loops.ts.json @@ -0,0 +1 @@ +{ "plain": 4400, "san": 5000 } From 883f22ddd62005a26bb0fff3e9b83991e3cf6b7e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 04:50:13 -0500 Subject: [PATCH 102/395] Run two-counter for loops on integer counters `for (let lo = a, hi = b; lo < hi; lo++, hi--)` kept both counters as doubles: the counted-loop match takes a single counter, so every `a[lo]`/`a[hi]` converted and tested its index. A loop whose two counters close in on each other by positive literal steps, are not written by the body and index something now stores both as i64: on every body entry both lie between the first start and the second. A start without a proven integer range is tested at run time on a versioned loop, as a counted loop's is. --- .../src/backend/llvm/counted-loops.ts | 81 ++++++++----- .../src/backend/llvm/emit-statements.ts | 58 ++++++++- .../compiler/src/ir/integer-loops.test.ts | 73 ++++++++++- packages/compiler/src/ir/integer-loops.ts | 114 ++++++++++++++++++ .../corpus/7140-paired-loop-counters.ts.json | 6 + tests/corpus/7140-paired-loop-counters.ts | 81 +++++++++++++ .../7140-paired-loop-counters.ts.json | 1 + 7 files changed, 377 insertions(+), 37 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json create mode 100644 tests/corpus/7140-paired-loop-counters.ts create mode 100644 tests/harness/corpus-costs/7140-paired-loop-counters.ts.json diff --git a/packages/compiler/src/backend/llvm/counted-loops.ts b/packages/compiler/src/backend/llvm/counted-loops.ts index 36fc63e1ff..02943b8523 100644 --- a/packages/compiler/src/backend/llvm/counted-loops.ts +++ b/packages/compiler/src/backend/llvm/counted-loops.ts @@ -1,4 +1,5 @@ -import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; +import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; +import type { IrExpr } from "../../ir/ir.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { f64Lit } from "./common.js"; @@ -59,37 +60,51 @@ export function emitCountedLoopSafety( `${safe} = fcmp ${loop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(loop.guardLimit)}`, ); } - if (loop.startGuarded) { - // An exact integer of at most 2^53 - 1 in magnitude, and not -0 - // (the bit patterns of the value and its round trip agree). - // NaN and infinities fail the ordered range tests. - const start = host.emitExpr(loop.start); - const low = B.tmp(), - high = B.tmp(), - raw = B.tmp(), - wide = B.tmp(), - back = B.tmp(), - bits = B.tmp(), - backBits = B.tmp(), - exact = B.tmp(), - inRange = B.tmp(), - ok = B.tmp(); - B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`); - B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`); - B.line(`${inRange} = and i1 ${low}, ${high}`); - B.line(`${raw} = fptosi double ${start.name} to i64`); - B.line(`${wide} = freeze i64 ${raw}`); - B.line(`${back} = sitofp i64 ${wide} to double`); - B.line(`${bits} = bitcast double ${start.name} to i64`); - B.line(`${backBits} = bitcast double ${back} to i64`); - B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); - B.line(`${ok} = and i1 ${inRange}, ${exact}`); - if (safe === "true") safe = ok; - else { - const both = B.tmp(); - B.line(`${both} = and i1 ${safe}, ${ok}`); - safe = both; - } - } + if (loop.startGuarded) safe = andSafe(host, safe, emitExactStart(host, loop.start)); + return safe; +} + +/** The run-time test of a guarded paired loop: each unproven start is an + * exact safe integer. */ +export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPairedForLoop): string { + let safe = "true"; + for (const counter of [loop.low, loop.high]) + if (counter.startGuarded) safe = andSafe(host, safe, emitExactStart(host, counter.start)); return safe; } + +function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string { + if (safe === "true") return ok; + const both = host.B.tmp(); + host.B.line(`${both} = and i1 ${safe}, ${ok}`); + return both; +} + +/** An exact integer of at most 2^53 - 1 in magnitude, and not -0 (the bit + * patterns of the value and its round trip agree). NaN and infinities fail + * the ordered range tests. */ +function emitExactStart(host: LlvmEmitterContext, expr: IrExpr): string { + const B = host.B; + const start = host.emitExpr(expr); + const low = B.tmp(), + high = B.tmp(), + raw = B.tmp(), + wide = B.tmp(), + back = B.tmp(), + bits = B.tmp(), + backBits = B.tmp(), + exact = B.tmp(), + inRange = B.tmp(), + ok = B.tmp(); + B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`); + B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`); + B.line(`${inRange} = and i1 ${low}, ${high}`); + B.line(`${raw} = fptosi double ${start.name} to i64`); + B.line(`${wide} = freeze i64 ${raw}`); + B.line(`${back} = sitofp i64 ${wide} to double`); + B.line(`${bits} = bitcast double ${start.name} to i64`); + B.line(`${backBits} = bitcast double ${back} to i64`); + B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); + B.line(`${ok} = and i1 ${inRange}, ${exact}`); + return ok; +} diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 7f189c066c..7239db7d89 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -42,10 +42,15 @@ import { matchIntegerArrayForLoop, matchIntegerBytesForLoop, matchIntegerCountedForLoop, + matchIntegerPairedForLoop, } from "../../ir/integer-loops.js"; import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js"; import { countedDenseShape, emitDenseLoopGuard, matchDenseArrayLoop } from "./dense-array-loops.js"; -import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js"; +import { + emitCountedLoopLimit, + emitCountedLoopSafety, + emitPairedLoopSafety, +} from "./counted-loops.js"; import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js"; import { mangleLocal } from "../mangle.js"; import { LlvmUnsupportedError } from "./unsupported.js"; @@ -868,6 +873,12 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.unaliasedArrays, ) : null; + // Two counters closing in on each other (`lo++, hi--`) take the same + // integer storage as a counted loop's one. + const pairedLoop = + !integerLoop && !countedLoop && emitter.countedLoopsEnabled + ? matchIntegerPairedForLoop(s, emitter.numericLocals, emitter.integerRanges) + : null; const window = countedLoop && emitter.byteWindowsEnabled ? matchByteWindow( @@ -1000,8 +1011,10 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { break; } let countedJoin: string | null = null; - if (countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) { - const safe = emitCountedLoopSafety(emitter, countedLoop); + if ((countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) || pairedLoop?.guarded) { + const safe = countedLoop + ? emitCountedLoopSafety(emitter, countedLoop) + : emitPairedLoopSafety(emitter, pairedLoop!); const fast = B.newLabel("counted.fast"); const slow = B.newLabel("counted.slow"); countedJoin = B.newLabel("counted.done"); @@ -1016,6 +1029,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.scopes.push([]); let integerSlot: string | null = null; let countedLimit: string | null = null; + const pairedSlots: string[] = []; if (integerLoop) { integerSlot = B.slot(); B.entryAllocas.push( @@ -1060,6 +1074,24 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { : countedLoop.range, }); countedLimit = emitCountedLoopLimit(emitter, countedLoop); + } else if (pairedLoop) { + for (const counter of [pairedLoop.low, pairedLoop.high]) { + const slot = B.slot(); + B.entryAllocas.push( + `${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(counter.localId)!.name}`, + ); + const value = emitter.emitExpr(counter.start); + const start = B.tmp(); + B.line(`${start} = fptosi double ${value.name} to i64`); + B.line(`store i64 ${start}, ptr ${slot}`); + pairedSlots.push(slot); + emitter.integerLoopBindings.set(counter.localId, { + slot, + type: "i64", + signed: true, + range: pairedLoop.range, + }); + } } else if (s.init) { // A multi-declarator head shares the loop's scope. Emitting its // IR block as an ordinary block would release captured/ref locals @@ -1141,6 +1173,14 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { `${inBounds} = icmp ${countedLoop.step > 0 ? "slt" : "sgt"} i64 ${index}, ${countedLimit}`, ); B.condBr(inBounds, lb, exit); + } else if (pairedLoop) { + const low = B.tmp(), + high = B.tmp(), + open = B.tmp(); + B.line(`${low} = load i64, ptr ${pairedSlots[0]}`); + B.line(`${high} = load i64, ptr ${pairedSlots[1]}`); + B.line(`${open} = icmp ${pairedLoop.inclusive ? "sle" : "slt"} i64 ${low}, ${high}`); + B.condBr(open, lb, exit); } else if (s.cond) B.condBr(emitter.emitCondition(s.cond), lb, exit); else B.br(lb); B.startBlock(lb); @@ -1172,6 +1212,14 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.line(`${old} = load i64, ptr ${integerSlot}`); B.line(`${next} = add nsw i64 ${old}, ${countedLoop.step}`); B.line(`store i64 ${next}, ptr ${integerSlot}`); + } else if (pairedLoop) { + [pairedLoop.low, pairedLoop.high].forEach((counter, i) => { + const old = B.tmp(); + const next = B.tmp(); + B.line(`${old} = load i64, ptr ${pairedSlots[i]}`); + B.line(`${next} = add nsw i64 ${old}, ${counter.step}`); + B.line(`store i64 ${next}, ptr ${pairedSlots[i]}`); + }); } else if (s.update) emitter.emitStmt(s.update); B.br(lc); } @@ -1180,6 +1228,10 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { emitter.releaseScope(emitter.scopes.pop()!); if (integerLoop) emitter.integerLoopBindings.delete(integerLoop.localId); if (countedLoop) emitter.integerLoopBindings.delete(countedLoop.localId); + if (pairedLoop) { + emitter.integerLoopBindings.delete(pairedLoop.low.localId); + emitter.integerLoopBindings.delete(pairedLoop.high.localId); + } if (integerArrayLoop) emitter.integerArrayBindings.delete(integerArrayLoop.localId); if (countedJoin) { B.br(countedJoin); diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index aada3b0535..24bbf5be0f 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -1,6 +1,10 @@ import { expect, test } from "vitest"; import { BOOL, F64, STRING, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "./ir.js"; -import { matchIntegerArrayForLoop, matchIntegerCountedForLoop } from "./integer-loops.js"; +import { + matchIntegerArrayForLoop, + matchIntegerCountedForLoop, + matchIntegerPairedForLoop, +} from "./integer-loops.js"; const loc = { file: "integer-array-loop.ts", start: 0, end: 0 }; const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); @@ -355,3 +359,70 @@ test("a descending loop from an array's last index takes an integer counter for range: { min: 0, max: 4294967294 }, }); }); + +test("paired counters closing in on each other run as integers", () => { + const vars = new Map(locals); + for (const id of ["lo", "hi", "head"]) vars.set(id, { id, name: id, type: F64, mutable: true }); + const step = (id: string, op: "+" | "-", value = 1): IrStmt => ({ + kind: "assign", + localId: id, + value: { kind: "bin", op, left: ref(id), right: number(value), type: F64, loc }, + loc, + }); + const element = (index: IrExpr): IrExpr => ({ + kind: "arrayGet", + arr: { kind: "varRef", localId: "a", type: arrayOf(F64), loc }, + index, + type: F64, + loc, + }); + const paired = ( + lowStart: IrExpr, + highStart: IrExpr, + update = [step("lo", "+"), step("hi", "-")], + ) => + ({ + kind: "for", + loc, + init: { + kind: "block", + body: [ + { kind: "varDecl", localId: "lo", init: lowStart, loc }, + { kind: "varDecl", localId: "hi", init: highStart, loc }, + ], + loc, + }, + cond: { kind: "bin", op: "<", left: ref("lo"), right: ref("hi"), type: BOOL, loc }, + update: { kind: "block", body: update, loc }, + body: [{ kind: "exprStmt", expr: element(ref("hi")), loc }], + }) as IrStmt & { kind: "for" }; + expect(matchIntegerPairedForLoop(paired(number(0), number(9)), vars)).toMatchObject({ + low: { localId: "lo", step: 1, startGuarded: false }, + high: { localId: "hi", step: -1, startGuarded: false }, + guarded: false, + range: { min: 0, max: 9 }, + }); + // A start without a known range is tested at run time. + expect(matchIntegerPairedForLoop(paired(number(0), ref("head")), vars)).toMatchObject({ + high: { startGuarded: true }, + guarded: true, + }); + // Counters moving the same way, written by the body, captured, started + // from each other, or never used as an index are refused. + expect( + matchIntegerPairedForLoop( + paired(number(0), number(9), [step("lo", "+"), step("hi", "+")]), + vars, + ), + ).toBeNull(); + const writing = paired(number(0), number(9)); + writing.body.push(step("hi", "-", 2)); + expect(matchIntegerPairedForLoop(writing, vars)).toBeNull(); + const boxed = new Map(vars); + boxed.set("hi", { ...vars.get("hi")!, boxed: true }); + expect(matchIntegerPairedForLoop(paired(number(0), number(9)), boxed)).toBeNull(); + expect(matchIntegerPairedForLoop(paired(number(0), ref("lo")), vars)).toBeNull(); + const unused = paired(number(0), number(9)); + unused.body = []; + expect(matchIntegerPairedForLoop(unused, vars)).toBeNull(); +}); diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index 9b46756856..e80680d6e1 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -281,6 +281,120 @@ export function matchIntegerCountedForLoop( }; } +/** A for loop whose two counters close in on each other: + * + * for (let lo = a, hi = b; lo < hi; lo++, hi--) { ... } + * + * The first counter only grows and the second only shrinks, by positive + * literal steps, the body writes neither, and the loop runs while the first + * is below (or at most) the second. Every body entry therefore holds both + * between the first start and the second start: exact integers when the + * starts are. A start without a proven integer range is tested at run time + * on a versioned loop, as a counted loop's is. */ +export interface IntegerPairedCounter { + localId: string; + start: IrExpr; + /** The signed step of each update. */ + step: number; + startGuarded: boolean; +} +export interface IntegerPairedForLoop { + low: IntegerPairedCounter; + high: IntegerPairedCounter; + inclusive: boolean; + guarded: boolean; + /** Both counters' values on every body entry. */ + range: IntegerRange; +} + +export function matchIntegerPairedForLoop( + stmt: IrStmt & { kind: "for" }, + locals: ReadonlyMap, + ranges: IntegerRanges = new Map(), +): IntegerPairedForLoop | null { + const init = stmt.init, + update = stmt.update, + cond = stmt.cond; + if (init?.kind !== "block" || init.body.length !== 2) return null; + if (update?.kind !== "block" || update.body.length !== 2) return null; + if (cond?.kind !== "bin" || (cond.op !== "<" && cond.op !== "<=")) return null; + if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null; + const lowId = cond.left.localId, + highId = cond.right.localId; + if (lowId === highId) return null; + const counter = (id: string): IntegerPairedCounter | null => { + const decl = init.body.find((d) => d.kind === "varDecl" && d.localId === id); + if (decl?.kind !== "varDecl" || !decl.init) return null; + const local = locals.get(id); + if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null; + const start = decl.init; + // Each start is evaluated once in the head and again on a fallback path. + if ( + !stableLoopValue(start, locals, stmt.body, false, lowId) || + !stableLoopValue(start, locals, stmt.body, false, highId) + ) + return null; + const steps = update.body.map((u) => constantStep(u, id)).filter((v) => v !== null); + if (steps.length !== 1 || writesLocal(stmt.body, id)) return null; + const known = + start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0) + ? { min: start.value, max: start.value } + : ranges.get(start); + const startGuarded = !known && start.kind !== "numLit" && start.type.kind === "f64"; + if (!known && !startGuarded) return null; + if (known && (known.min < -Number.MAX_SAFE_INTEGER || known.max > Number.MAX_SAFE_INTEGER)) + return null; + return { localId: id, start, step: steps[0]!, startGuarded }; + }; + const low = counter(lowId), + high = counter(highId); + if (!low || !high || low.step <= 0 || high.step >= 0) return null; + const startRange = (c: IntegerPairedCounter): IntegerRange => + c.startGuarded + ? { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER } + : c.start.kind === "numLit" + ? { min: c.start.value, max: c.start.value } + : ranges.get(c.start)!; + const guarded = low.startGuarded || high.startGuarded; + // Only a loop whose counters index something gains from integer storage. + let useful = false, + cost = 0; + const isCounter = (e: IrExpr): boolean => + (e.kind === "varRef" && (e.localId === lowId || e.localId === highId)) || + (e.kind === "bin" && + (e.op === "+" || e.op === "-") && + (isCounter(e.left) || isCounter(e.right))); + const expr = (e: IrExpr): boolean => { + if (e.kind === "arrayGet" && isCounter(e.index)) useful = true; + if (e.kind === "strIntrinsic" && e.method === "charCodeAt" && e.args[0] && isCounter(e.args[0])) + useful = true; + if (e.kind === "bytesIntrinsic" && e.method === "get" && e.args[0] && isCounter(e.args[0])) + useful = true; + return (!guarded || ++cost <= 80) && everyExprChild(e, expr, bodyStmt); + }; + const bodyStmt = (s: IrStmt): boolean => { + if ((s.kind === "arraySet" || s.kind === "bytesSet") && isCounter(s.index)) useful = true; + if ( + guarded && + (s.kind === "for" || s.kind === "forOf" || s.kind === "while" || s.kind === "doWhile") + ) + return false; + return (!guarded || ++cost <= 80) && everyStmtChild(s, expr, bodyStmt); + }; + if (!stmt.body.every(bodyStmt) || !useful) return null; + return { + low, + high, + inclusive: cond.op === "<=", + guarded, + // A loop whose starts already fail the condition never enters the body. + range: { + min: startRange(low).min, + max: Math.max(startRange(low).min, startRange(high).max), + }, + }; +} + /** True when a lowered subtree writes `localId`. Local ids are unique per * function, so typed traversal includes writes * nested in expressions, branches, nested loops, and try/finally bodies. */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json new file mode 100644 index 0000000000..122d64bb68 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7140-paired-loop-counters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7140-paired-loop-counters.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7140-paired-loop-counters.ts b/tests/corpus/7140-paired-loop-counters.ts new file mode 100644 index 0000000000..ec1610d1a9 --- /dev/null +++ b/tests/corpus/7140-paired-loop-counters.ts @@ -0,0 +1,81 @@ +// For loops with two counters closing in on each other (`lo++, hi--`): +// in-place reversals, inclusive bounds, larger steps, early exits, starts +// that are fractions, negative zero, NaN, huge or already crossed, nested +// reversals, and counters a closure captures or the body writes. +function reverse(a: number[], from: number, to: number): number[] { + for (let lo = from, hi = to; lo < hi; lo++, hi--) { + const t = a[lo]!; + a[lo] = a[hi]!; + a[hi] = t; + } + return a; +} +const base = (): number[] => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; +console.log(reverse(base(), 0, 9).join(",")); +console.log(reverse(base(), 2, 6).join(",")); +console.log(reverse(base(), 6, 2).join(",")); +console.log(reverse(base(), 3, 3).join(",")); +// Unproven starts: fractions, negative zero, NaN and huge values keep JS +// semantics (charCodeAt truncates its index; out-of-range reads are NaN). +function codes(s: string, from: number, to: number): string { + const out: number[] = []; + for (let lo = from, hi = to; lo < hi; lo++, hi--) out.push(s.charCodeAt(lo), s.charCodeAt(hi), lo, hi); + return out.join(","); +} +const word = "abcdefgh"; +console.log(codes(word, 0.5, 4.5)); +console.log(codes(word, -0, 3), Object.is(-0 + 0, 0)); +console.log(codes(word, NaN, 4), "|", codes(word, 0, NaN)); +console.log(codes(word, 2 ** 53 - 3, 2 ** 53)); +console.log(codes(word, -3, 2)); + +function pairs(n: number): string { + const out: string[] = []; + for (let i = 0, j = n; i <= j; i += 2, j -= 3) out.push(`${i}:${j}`); + return out.join(" "); +} +console.log(pairs(20), "|", pairs(0), "|", pairs(-1)); + +function firstGap(a: number[]): number { + let found = -1; + for (let lo = 0, hi = a.length - 1; lo < hi; lo++, hi--) { + if (a[lo] === a[hi]) continue; + found = lo; + break; + } + return found; +} +console.log(firstGap([1, 2, 3, 2, 1]), firstGap([1, 2, 3, 4, 2, 1]), firstGap([])); + +// The flip loop of a permutation puzzle: the next index comes from the array. +function flips(perm: number[]): number { + const work = perm.slice(); + let count = 0; + let head = work[0]!; + while (head !== 0) { + for (let lo = 0, hi = head; lo < hi; lo++, hi--) { + const t = work[lo]!; + work[lo] = work[hi]!; + work[hi] = t; + } + count++; + head = work[0]!; + } + return count; +} +console.log(flips([3, 1, 0, 2]), flips([4, 3, 2, 1, 0]), flips([0, 1, 2])); + +// Counters a closure reads, or the body writes, stay ordinary bindings. +const readers: (() => number)[] = []; +for (let lo = 0, hi = 4; lo < hi; lo++, hi--) readers.push(() => lo * 10 + hi); +console.log(readers.map((f) => f()).join(",")); +const seen: number[] = []; +for (let lo = 0, hi = 10; lo < hi; lo++, hi--) { + seen.push(lo, hi); + if (lo === 2) hi -= 3; +} +console.log(seen.join(",")); +const text = "abcdefgh"; +let mirrored = ""; +for (let lo = 0, hi = text.length - 1; lo <= hi; lo++, hi--) mirrored += text.charAt(hi) + text.charCodeAt(lo); +console.log(mirrored); diff --git a/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json b/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json new file mode 100644 index 0000000000..549fd6a4ae --- /dev/null +++ b/tests/harness/corpus-costs/7140-paired-loop-counters.ts.json @@ -0,0 +1 @@ +{ "plain": 3880, "san": 6000 } From 13608301496f6a1980b63bb08f6ac3391d6d957e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 04:52:44 -0500 Subject: [PATCH 103/395] Support new tls.TLSSocket and tls.connect({ socket }) --- .../src/generated/node-v24-compatibility.json | 12 +- .../generated/node-v24-internal.json | 23 +- internal/compatibility/static-support.json | 1 + .../ambient/scriptc-node-fallback.d.ts | 63 +++- packages/compiler/src/backend/llvm/lib-tls.ts | 7 + .../src/frontend/lowering/lower-classes.ts | 8 +- .../src/frontend/lowering/lower-server.ts | 162 ++++++++- packages/compiler/src/ir/builtin-effects.ts | 2 + .../compiler/src/ir/builtin-signatures.ts | 14 + .../tests/corpus/7087-tls-socket-wrap.ts.json | 6 + .../7088-tls-socket-connect-tunnel.ts.json | 6 + .../7089-http-host-header-tunnels.ts.json | 6 + .../diagnostics/tls-socket-fences.ts.json | 6 + .../node-types/tls-socket-wrap.ts.json | 6 + packages/runtime/src/scr_http.c | 39 ++- packages/runtime/src/scr_net.c | 313 ++++++++++++++---- packages/runtime/src/scr_net.h | 24 ++ packages/runtime/src/scr_tls.c | 282 +++++++++++++++- packages/runtime/src/scr_tls.h | 9 + tests/corpus/7087-tls-socket-wrap.ts | 104 ++++++ .../corpus/7088-tls-socket-connect-tunnel.ts | 64 ++++ tests/corpus/7089-http-host-header-tunnels.ts | 31 ++ tests/diagnostics/tls-socket-fences.ts | 20 ++ tests/fixtures/node-types/tls-socket-wrap.ts | 31 ++ .../__snapshots__/tls-socket-fences.ts.txt | 26 ++ .../corpus-costs/7087-tls-socket-wrap.ts.json | 1 + .../7088-tls-socket-connect-tunnel.ts.json | 1 + .../7089-http-host-header-tunnels.ts.json | 1 + tests/harness/errors.test.ts | 50 +++ tests/harness/project-config.test.ts | 18 + 30 files changed, 1223 insertions(+), 113 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7087-tls-socket-wrap.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7088-tls-socket-connect-tunnel.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7089-http-host-header-tunnels.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/tls-socket-fences.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-socket-wrap.ts.json create mode 100644 tests/corpus/7087-tls-socket-wrap.ts create mode 100644 tests/corpus/7088-tls-socket-connect-tunnel.ts create mode 100644 tests/corpus/7089-http-host-header-tunnels.ts create mode 100644 tests/diagnostics/tls-socket-fences.ts create mode 100644 tests/fixtures/node-types/tls-socket-wrap.ts create mode 100644 tests/harness/__snapshots__/tls-socket-fences.ts.txt create mode 100644 tests/harness/corpus-costs/7087-tls-socket-wrap.ts.json create mode 100644 tests/harness/corpus-costs/7088-tls-socket-connect-tunnel.ts.json create mode 100644 tests/harness/corpus-costs/7089-http-host-header-tunnels.ts.json diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 682e68f994..1d9190afe8 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -67806,9 +67806,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 documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -68565,9 +68565,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 documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index a86967ff38..58deb89448 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -95421,7 +95421,10 @@ "tests/corpus/2557-tls-ca-store.ts", "tests/corpus/2596-net-arg-ladders.cjs", "tests/corpus/6721-tls-alpn-protocols.ts", - "tests/corpus/6722-tls-min-max-version.ts" + "tests/corpus/6722-tls-min-max-version.ts", + "tests/corpus/7087-tls-socket-wrap.ts", + "tests/corpus/7088-tls-socket-connect-tunnel.ts", + "tests/fixtures/node-types/tls-socket-wrap.ts" ] }, "anchorSource": "chapter" @@ -96123,8 +96126,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-feature:tls.tls.TLSSocket", + "tests": [ + "tests/corpus/7087-tls-socket-wrap.ts", + "tests/corpus/7088-tls-socket-connect-tunnel.ts", + "tests/fixtures/node-types/tls-socket-wrap.ts" + ] }, "anchorSource": "exact" }, @@ -96911,8 +96919,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:tls" + "status": "partial", + "evidence": "compiler-feature:tls.'secure'", + "tests": [ + "tests/corpus/7087-tls-socket-wrap.ts", + "tests/corpus/7088-tls-socket-connect-tunnel.ts", + "tests/fixtures/node-types/tls-socket-wrap.ts" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 6b3eaab4c6..6a9a1dbad3 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -369,6 +369,7 @@ { "chapter": "console", "signatures": ["Class: `Console`"], "status": "partial", "evidence": ["tests/corpus/4900-console-instances.ts", "tests/corpus/4901-console-instance-errors.ts"] }, { "chapter": "tls", "signatures": ["`alpnProtocol` Type: {string|boolean|null}"], "status": "partial", "evidence": ["tests/corpus/6721-tls-alpn-protocols.ts"] }, { "chapter": "tls", "signatures": ["`tlsSocket.getProtocol()`"], "status": "partial", "evidence": ["tests/corpus/6722-tls-min-max-version.ts"] }, + { "chapter": "tls", "signatures": ["Class: `tls.TLSSocket`", "Event: `'secure'`"], "status": "partial", "evidence": ["tests/corpus/7087-tls-socket-wrap.ts", "tests/corpus/7088-tls-socket-connect-tunnel.ts", "tests/fixtures/node-types/tls-socket-wrap.ts"] }, { "chapter": "webstreams", "signatures": ["`readableStream.values([options])`"], "status": "partial", "evidence": ["tests/corpus/2862-web-readable-async-iteration.ts"] }, { "chapter": "webstreams", "signatures": ["`streamConsumers.text(stream)`", "`streamConsumers.json(stream)`", "`streamConsumers.buffer(stream)`"], "status": "partial", "evidence": ["tests/corpus/2629-stream-consumers.ts", "tests/corpus/2630-stream-consumers-errors.ts"] }, { "chapter": "webstreams", "signatures": ["`streamConsumers.arrayBuffer(stream)`"], "status": "partial", "evidence": ["tests/corpus/4875-stream-consumers-array-buffer.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 936283d407..4e8fbde15a 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -4858,8 +4858,8 @@ declare module "net" { * unshift — read answers exactly n buffered bytes or null. */ read(size?: number): Buffer | null; unshift(chunk: Uint8Array): void; - /* secureConnect/session are TLS-socket events declared on the shared - * socket surface (the lowering collapses TLSSocket onto this kind): + /* secureConnect/secure/session are TLS-socket events declared on the + * shared socket surface (the lowering collapses TLSSocket onto this kind): * the runtime gates on the transport, so a plain socket's * registration never fires — Node's exact split. 'session' fires * once with the serialized session (the received-ticket event). */ @@ -4867,7 +4867,15 @@ declare module "net" { /* After setEncoding the chunk is a string. */ on(event: "data", listener: (chunk: string) => void): void; on( - event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect", + event: + | "end" + | "connect" + | "ready" + | "timeout" + | "readable" + | "finish" + | "secureConnect" + | "secure", listener: () => void, ): void; on(event: "close", listener: (hadError: boolean) => void): void; @@ -4883,7 +4891,15 @@ declare module "net" { /* After setEncoding the chunk is a string. */ addListener(event: "data", listener: (chunk: string) => void): void; addListener( - event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect", + event: + | "end" + | "connect" + | "ready" + | "timeout" + | "readable" + | "finish" + | "secureConnect" + | "secure", listener: () => void, ): void; addListener(event: "close", listener: (hadError: boolean) => void): void; @@ -4898,7 +4914,15 @@ declare module "net" { /* After setEncoding the chunk is a string. */ once(event: "data", listener: (chunk: string) => void): void; once( - event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect", + event: + | "end" + | "connect" + | "ready" + | "timeout" + | "readable" + | "finish" + | "secureConnect" + | "secure", listener: () => void, ): void; once(event: "close", listener: (hadError: boolean) => void): void; @@ -4949,9 +4973,10 @@ declare module "net" { on(event: "timeout", listener: (socket: Socket) => void): void; on(event: "error", listener: (err: Error) => void): void; /* The WebSocket handover: fires INSTEAD of 'request' for - * Connection: upgrade requests, with the raw socket + head bytes. */ + * Connection: upgrade requests, with the raw socket + head bytes; + * 'connect' is its twin for CONNECT requests (HTTP tunneling). */ on( - event: "upgrade", + event: "upgrade" | "connect", listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void, ): void; /* http.createServer(handler)'s event twin (a no-parser net server @@ -4967,7 +4992,7 @@ declare module "net" { addListener(event: "timeout", listener: (socket: Socket) => void): void; addListener(event: "error", listener: (err: Error) => void): void; addListener( - event: "upgrade", + event: "upgrade" | "connect", listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void, ): void; addListener( @@ -4980,7 +5005,7 @@ declare module "net" { once(event: "timeout", listener: (socket: Socket) => void): void; once(event: "error", listener: (err: Error) => void): void; once( - event: "upgrade", + event: "upgrade" | "connect", listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void, ): void; once( @@ -5451,6 +5476,7 @@ declare module "tls" { * split — authorizationError is the verify-failure CODE STRING * (DEPTH_ZERO_SELF_SIGNED_CERT, ...), or null. */ export interface TLSSocket extends Socket { + readonly encrypted: true; authorized: boolean; authorizationError: string | null; /* null before the handshake, then the negotiated protocol or false */ @@ -5462,6 +5488,21 @@ declare module "tls" { /* whether the handshake resumed a cached session */ isSessionReused(): boolean; } + /** new tls.TLSSocket(socket, options): the server-mode wrap of an open + * socket (a server connection or an http CONNECT socket). The options + * record reads at runtime; isServer, cert, key, secureContext, + * ALPNProtocols, minVersion and maxVersion are implemented. */ + export interface TLSSocketOptions { + isServer?: boolean; + cert?: string | Uint8Array; + key?: string | Uint8Array; + secureContext?: SecureContext; + ALPNProtocols?: string[] | Uint8Array; + minVersion?: string; + maxVersion?: string; + [option: string]: unknown; + } + export const TLSSocket: new (socket: Socket, options: TLSSocketOptions) => TLSSocket; export interface CipherNameAndProtocol { name: string; standardName: string; @@ -5470,12 +5511,14 @@ declare module "tls" { export const DEFAULT_MIN_VERSION: string; export const DEFAULT_MAX_VERSION: string; /** tls.connect options — the RUNTIME options record (members read at - * runtime; port/host/rejectUnauthorized/ca/servername are the + * runtime; port/host/socket/rejectUnauthorized/ca/servername are the * honestly-implemented set, every other documented member throws the * runtime fence — the options-record stance's divergence-66 shape). */ export interface ConnectionOptions { port?: number; host?: string; + /* an open socket to run the client handshake over */ + socket?: Socket; rejectUnauthorized?: boolean; ca?: string | Uint8Array | ReadonlyArray; servername?: string; diff --git a/packages/compiler/src/backend/llvm/lib-tls.ts b/packages/compiler/src/backend/llvm/lib-tls.ts index c5c77d3a6e..1d1cf659c4 100644 --- a/packages/compiler/src/backend/llvm/lib-tls.ts +++ b/packages/compiler/src/backend/llvm/lib-tls.ts @@ -121,6 +121,13 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu case "tls.sockOnSecureConnect": host.toHeap(args[1]!); return finishRuntimeCall(host, e, "scr_tls_sock_on_secure_connect", a); + case "tls.sockOnSecure": + host.toHeap(args[1]!); + return finishRuntimeCall(host, e, "scr_tls_sock_on_secure", a); + case "tls.wrapSocket": + return finishRuntimeCall(host, e, "scr_tls_wrap_socket", [a[0]!, a[1]!, ptr()]); + case "tls.wrapSocketCtx": + return finishRuntimeCall(host, e, "scr_tls_wrap_socket", a); case "tls.sockOnSession": { const cb = args[1]!, type = callbackType(cb); diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 2a772ae253..d8ccc15b7b 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -158,7 +158,7 @@ import { classSymbolKeyOfSymbol, symbolFieldDisplayName, } from "./symbol-fields.js"; -import { lowerHttpAgentNew, lowerHttpServerNew } from "./lower-server.js"; +import { lowerHttpAgentNew, lowerHttpServerNew, lowerTlsSocketNew } from "./lower-server.js"; import { ambientNsRootOf, ambientUndefReadType, @@ -9836,6 +9836,12 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const httpServer = lowerHttpServerNew(lowerer, expr); if (httpServer) return httpServer; } + // `new tls.TLSSocket(socket, options)` — the server-mode wrap of an open + // socket (lower-server). + { + const tlsSocket = lowerTlsSocketNew(lowerer, expr); + if (tlsSocket) return tlsSocket; + } // `new http.Agent(opts?)` / `new https.Agent(opts?)` — the Agent // handle (lower-server): getName/destroy/counters through the dyn // handle ops, requests thread it via the agent option. diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 94d49d8b37..d9746e1f66 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -2939,6 +2939,26 @@ function lowerNetSocketMethodCall( loc, }; } + if (event === "secure") { + // The handshake's completion on a TLS socket (after secureConnect on + // a client); plain sockets never fire it. + const { cb } = lowerCallbackArg( + lowerer, + args[1]!, + "secure listeners", + 0, + () => false, + "use ()", + [], + ); + return { + kind: "libCall", + fn: "tls.sockOnSecure", + args: [receiver, cb, once], + type: VOID, + loc, + }; + } if (event === "session") { // The received-ticket event: fires once per TLS client socket with // the serialized session (a Buffer); plain sockets never fire it. @@ -2962,7 +2982,7 @@ function lowerNetSocketMethodCall( lowerer.noLowering( `socket.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, args[0]!, - '"data", "end", "close", "error", "connect", "lookup", "ready", "timeout", "readable", "secureConnect", and "session" are the supported socket events (as literals)', + '"data", "end", "close", "error", "connect", "lookup", "ready", "timeout", "readable", "secureConnect", "secure", and "session" are the supported socket events (as literals)', ); } lowerer.noLowering( @@ -4676,6 +4696,125 @@ export function lowerHttpServerNew(lowerer: Lowerer, expr: ts.NewExpression): Ir return lowerHttpCreateServerForms(lowerer, expr, "new http.Server", locOf(expr)); } +/** `new tls.TLSSocket(socket, options)` in server mode: a new TLS socket + * takes over the socket's connection and runs the server handshake + * (scr_tls_wrap_socket). The options record reads at runtime, the + * tls.connect precedent. A literal's secureContext member is a + * SecureContext handle, which the checked-dynamic record cannot hold, so + * it passes as its own argument; members after it must be free of side + * effects, because it is read after them. Node never starts the handshake + * of a client-mode TLSSocket, so a literal without isServer: true fences + * here. */ +export function lowerTlsSocketNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { + const callee = expr.expression; + const isTlsSocket = + (ts.isPropertyAccessExpression(callee) && + callee.name.text === "TLSSocket" && + lowerer.builtinMemberOf(callee)?.module === "tls") || + (ts.isIdentifier(callee) && + lowerer.builtinImportOf(callee)?.module === "tls" && + lowerer.builtinImportOf(callee)?.member === "TLSSocket"); + if (!isTlsSocket) return null; + const loc = locOf(expr); + const HINT = + "the supported forms are new tls.TLSSocket(socket, { isServer: true, cert, key }) and new tls.TLSSocket(socket, { isServer: true, secureContext })"; + const args = expr.arguments?.nodes ?? []; + if (args.length !== 2 || args.some(ts.isSpreadElement)) { + lowerer.noLowering(`new tls.TLSSocket with ${args.length} argument(s)`, expr, HINT); + } + const socket = lowerer.lowerExpr(args[0]!); + if (socket.type.kind !== "netSocket") { + lowerer.noLowering( + `new tls.TLSSocket over a '${lowerer.fmt(socket.type)}' value`, + args[0]!, + "the wrapped value must be a net.Socket (a server connection or an http CONNECT socket)", + ); + } + let optsNode: ts.Expression = args[1]!; + while (ts.isParenthesizedExpression(optsNode)) optsNode = optsNode.expression; + if (!ts.isObjectLiteralExpression(optsNode)) { + const o = lowerer.lowerExpr(optsNode); + const opts: IrExpr = + o.type.kind === "dyn" + ? o + : canConvertToDyn( + o.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ? { kind: "dynFrom", value: o, type: DYN, loc } + : lowerer.noLowering( + `new tls.TLSSocket with a '${lowerer.fmt(o.type)}' options record`, + optsNode, + `${HINT} — a secureContext member lowers in an options object literal`, + ); + return { kind: "libCall", fn: "tls.wrapSocket", args: [socket, opts], type: NETSOCKET_T, loc }; + } + const props = optsNode.properties.nodes; + const nameOf = (p: ts.ObjectLiteralElementLike): string | null => + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) + ? p.name.text + : null; + const isServer = props.find((p) => nameOf(p) === "isServer"); + if ( + isServer === undefined || + (ts.isPropertyAssignment(isServer) && isServer.initializer.kind === ts.SyntaxKind.FalseKeyword) + ) { + lowerer.noLowering( + "new tls.TLSSocket in client mode", + optsNode, + "Node starts no handshake for a client-mode TLSSocket — pass isServer: true for the server side; tls.connect makes client sockets", + ); + } + const ctxIndex = props.findIndex((p) => nameOf(p) === "secureContext"); + if (ctxIndex < 0) { + const opts = lowerDynObjectLiteral(lowerer, optsNode); + return { kind: "libCall", fn: "tls.wrapSocket", args: [socket, opts], type: NETSOCKET_T, loc }; + } + const ctxProp = props[ctxIndex]!; + const ctxNode = ts.isShorthandPropertyAssignment(ctxProp) + ? ctxProp.name + : (ctxProp as ts.PropertyAssignment).initializer; + for (const p of props.slice(ctxIndex + 1)) { + const pure = + ts.isShorthandPropertyAssignment(p) || + (ts.isPropertyAssignment(p) && sideEffectFreeOptionValue(p.initializer)); + if (!pure) { + lowerer.noLowering( + "a new tls.TLSSocket options literal with side effects after secureContext", + p, + "secureContext is read after the other members — move it to the end of the object", + ); + } + } + let ctx: IrExpr | null = null; + const opts = lowerDynObjectLiteral(lowerer, optsNode, (node, value) => { + if (node !== ctxNode) return lowerer.coerceToExpected(value, DYN); + if (value.type.kind !== "secureCtx") { + lowerer.noLowering( + `a '${lowerer.fmt(value.type)}' secureContext`, + node, + "pass a tls.createSecureContext({ cert, key }) result", + ); + } + ctx = value; + return { + kind: "dynFrom", + value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type: DYN, + loc, + }; + }); + return { + kind: "libCall", + fn: "tls.wrapSocketCtx", + args: [socket, opts, ctx!], + type: NETSOCKET_T, + loc, + }; +} + /** Module-function calls on http import bindings: createServer([options][, * handler]) and the call-form Server (Node's constructor works without * `new` — test/parallel's http.Server(fn) spelling). The handler takes @@ -4985,14 +5124,14 @@ function lowerTlsServerOptionsOrDyn( * connect(options[, cb]) and connect(port[, host][, options][, cb]) with * a RUNTIME options record (the checked-dynamic JS lane's shape — a dyn * value, or a record that converts): the runtime walk implements - * port/host/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout and throws the catchable + * port/host/socket/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout and throws the catchable * fence for other documented members (the divergence-66 stance). The * callback (and 'secureConnect'/'connect' listeners) fires * post-handshake — Node's secureConnect timing. */ function lowerTlsConnectCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { const args = expr.arguments.nodes; const FORMS_HINT = - "the supported forms are connect(options[, cb]) and connect(port[, host][, options][, cb]) — options implement port/host/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout"; + "the supported forms are connect(options[, cb]) and connect(port[, host][, options][, cb]) — options implement port/host/socket/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout"; if (args.length < 1 || args.length > 4 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`tls.connect with ${args.length} arguments`, expr, FORMS_HINT); } @@ -5043,7 +5182,22 @@ function lowerTlsConnectCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Src // The options value rides whole to the runtime walk: dyn passes // directly; a typed record that converts boxes into the checked-dynamic tree. let opts: IrExpr; - if (optsNode !== null) { + let literal: ts.Expression | null = optsNode; + while (literal !== null && ts.isParenthesizedExpression(literal)) literal = literal.expression; + if ( + literal !== null && + ts.isObjectLiteralExpression(literal) && + literal.properties.nodes.some( + (p) => + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + ts.isIdentifier(p.name) && + p.name.text === "socket", + ) + ) { + // A socket member is a handle the record form cannot box: the literal + // builds the checked-dynamic object member by member. + opts = lowerDynObjectLiteral(lowerer, literal); + } else if (optsNode !== null) { const o = lowerer.lowerExpr(optsNode); if (o.type.kind === "dyn") { opts = o; diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index dd82d06c2d..9f604e4084 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -230,6 +230,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "http2.createSecureServerDynCb", "tls.connect", "tls.connectCb", + "tls.wrapSocket", + "tls.wrapSocketCtx", // The CA-store surface: an unknown getCACertificates type throws // Node's ERR_INVALID_ARG_VALUE TypeError; setDefaultCACertificates // throws Node's ERR_CRYPTO_OPERATION_FAILED on a certificate-free diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 8f5928d12d..bcf4671118 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2182,6 +2182,20 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ result: VOID, }, "tls.sockOnSession": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID }, + /** The 'secure' event: the handshake completed (after secureConnect on + * a client). Plain sockets never fire it. */ + "tls.sockOnSecure": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + /** new tls.TLSSocket(socket, options) in server mode: a new TLS socket + * takes over the socket's connection (wrapSocketCtx passes a literal's + * secureContext member apart from the runtime options record). The + * record reads at runtime: isServer, cert, key, ALPNProtocols, + * minVersion and maxVersion are implemented, the client mode and other + * documented members throw the runtime fence. MAY THROW. */ + "tls.wrapSocket": { argTypes: [NETSOCKET_T, DYN], result: NETSOCKET_T }, + "tls.wrapSocketCtx": { argTypes: [NETSOCKET_T, DYN, SECURECTX_T], result: NETSOCKET_T }, // createSecureContext({ cert, key }) mints the opaque SNI-answer handle. /** tls.createSecureContext({ cert, key }) — parses the PEM pair into an * opaque SecureContext handle (secureCtx kind) for SNI callbacks to diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7087-tls-socket-wrap.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7087-tls-socket-wrap.ts.json new file mode 100644 index 0000000000..37009eaea4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7087-tls-socket-wrap.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7087-tls-socket-wrap.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7088-tls-socket-connect-tunnel.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7088-tls-socket-connect-tunnel.ts.json new file mode 100644 index 0000000000..71947ecdde --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7088-tls-socket-connect-tunnel.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7088-tls-socket-connect-tunnel.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7089-http-host-header-tunnels.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7089-http-host-header-tunnels.ts.json new file mode 100644 index 0000000000..9b30021731 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7089-http-host-header-tunnels.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7089-http-host-header-tunnels.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/tls-socket-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/tls-socket-fences.ts.json new file mode 100644 index 0000000000..409766067a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/tls-socket-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/tls-socket-fences.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-socket-wrap.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-socket-wrap.ts.json new file mode 100644 index 0000000000..68346a1150 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-socket-wrap.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/tls-socket-wrap.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index d9876c30b8..a5847e59b0 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -2918,22 +2918,6 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len, bool *o conn->trailer_bytes = 0; conn->trailer_fields = 0; - if (!http10 && conn->srv->require_host_header) { - bool has_host = false; - for (size_t i = 0; i < req->nheaders; i++) { - const ScrStr *name = req->hnames[i]; - if (name->len == 4 && memcmp(name->data, "host", 4) == 0) { - has_host = true; - break; - } - } - if (!has_host) { - scr_http_req_release(req); - scr_http_conn_missing_host(conn); - return true; - } - } - /* framing: chunked wins over Content-Length (RFC 9112) */ bool chunked = false; long long content_length = 0; @@ -2958,12 +2942,33 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len, bool *o } } + /* HTTP/1.1 requires Host (requireHostHeader). CONNECT requests, and + * upgrade requests an 'upgrade' listener takes, leave the parser before + * Node's check. */ + bool is_connect = req->method->len == 7 && memcmp(req->method->data, "CONNECT", 7) == 0; + bool takes_upgrade = has_upgrade_hdr && conn_upgrade_token && conn->srv->upgrade_ls.n > 0; + if (!http10 && conn->srv->require_host_header && !is_connect && !takes_upgrade) { + bool has_host = false; + for (size_t i = 0; i < req->nheaders; i++) { + const ScrStr *name = req->hnames[i]; + if (name->len == 4 && memcmp(name->data, "host", 4) == 0) { + has_host = true; + break; + } + } + if (!has_host) { + scr_http_req_release(req); + scr_http_conn_missing_host(conn); + return true; + } + } + /* A CONNECT request: 'connect' fires INSTEAD of 'request' — the * upgrade machinery's exact shape ((req, socket, head); the parser * steps aside; no listener destroys the socket, Node's default). The * h2 compat server's HTTP/1.1 arm lands here too; its HTTP/2 arm is * dispatched separately with the compatibility response handle. */ - if (req->method->len == 7 && memcmp(req->method->data, "CONNECT", 7) == 0) { + if (is_connect) { memmove(conn->buf, conn->buf + head_len, conn->len - head_len); conn->len -= head_len; if (!scr_net_ls_has_live(&conn->srv->connect_ls)) { diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index f091daede8..8f9a684248 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -517,6 +517,9 @@ struct ScrNetSocket { int remote_port_cache; bool remote_v6_cache; ScrStr *pending_err; /* deferred connect failure */ + /* pending_err's code when its message does not name one (static + * storage; NULL = the code the message maps to) */ + const char *pending_code; /* the caller-lookup dial (net.connect with a lookup option): true while * the lookup decides; dial_ips holds its answered addresses (+1 each), * dialed in order — a connect failure closes the fd and tries @@ -531,6 +534,9 @@ struct ScrNetSocket { size_t dial_n, dial_i; /* a host name the dial resolves first (+1; NULL for IP literals) */ ScrStr *dial_host; + /* the host a client dial was given, Node's socket._host (+1; NULL on + * server sockets): tls.connect({ socket }) verifies against it */ + ScrStr *host_name; /* the in-flight dial chain's LAST failure — surfaced (moved into * pending_err) only when the list exhausts; a later success drops it */ ScrStr *dial_err; @@ -607,6 +613,12 @@ struct ScrNetSocket { * until the proxy answers 200: writes buffer and the transport * handshake waits until the tunnel is up */ struct ScrNetTunnel *tunnel; + /* new tls.TLSSocket(socket) and tls.connect({ socket }): on the wrapped + * socket, the socket that took over its connection (+1; end() and + * destroy() act on it); on that socket, the one it took over (+1; its + * 'close' fires just before this socket's, as in Node) */ + struct ScrNetSocket *adopted_by; + struct ScrNetSocket *adopted_from; }; /* The CONNECT head still being sent from out_off, and the proxy's @@ -616,6 +628,9 @@ typedef struct ScrNetTunnel { size_t out_len, out_off; char *in; size_t in_len; + /* bytes a wrapped socket left unsent, which go out before the + * transport's own; no response head is read */ + bool send_only; } ScrNetTunnel; /* The 'lookup' event's half of the runtime, installed by the first @@ -712,6 +727,7 @@ void scr_net_sock_release(ScrNetSocket *s) { free(s->dial_ips); scr_str_release(s->dial_err); scr_str_release(s->dial_host); + scr_str_release(s->host_name); scr_str_release(s->unix_path); scr_net_ls_drop(&s->lookup_ls); if (s->lookup_evs != NULL) scr_net_lookup_ops->free(s); @@ -721,6 +737,8 @@ void scr_net_sock_release(ScrNetSocket *s) { if (s->pipe_dst) scr_net_sock_release(s->pipe_dst); if (s->conn_pending) scr_net_server_release(s->conn_pending); if (s->server) scr_net_server_release(s->server); + if (s->adopted_by) scr_net_sock_release(s->adopted_by); + if (s->adopted_from) scr_net_sock_release(s->adopted_from); if (s->fd >= 0) scr_net_close_fd_raw(s->fd); #ifdef SCR_RC_AUDIT scr_net_live--; @@ -962,7 +980,7 @@ static ScrNetSocket *scr_net_sock_new(void) { static void scr_net_sock_update_read(ScrNetSocket *s) { bool want = s->fd >= 0 && !s->connecting && !s->rd_eof && !s->user_paused && (s->data_ls.n > 0 || s->pipe_dst != NULL || s->native_data != NULL || - s->tunnel != NULL /* the proxy's CONNECT answer */ || + (s->tunnel != NULL && !s->tunnel->send_only) /* the proxy's CONNECT answer */ || s->flowing /* resume(): flow (and discard sans listeners) */ || s->readable_ls.n > 0 /* paused-mode consumer: bytes buffer */ || s->tops != NULL /* a transport is ALWAYS a consumer: the TLS @@ -977,7 +995,8 @@ static void scr_net_sock_update_read(ScrNetSocket *s) { * finished socket the same way */ || (!s->eof_pending && s->rlen - s->rhead < SCR_NET_READ_AHEAD) /* no consumer yet: read ahead into the buffer, as Node's - * stream does up to its high-water mark */); + * stream does up to its high-water mark */) && + !(s->tunnel != NULL && s->tunnel->send_only); if (want && !s->read_armed) { scr_net_watch_read(s->fd, s, true); s->read_armed = true; @@ -1056,6 +1075,23 @@ static void scr_net_sock_buffer(ScrNetSocket *s, const char *data, size_t len) { * dead or ended write half (Node fires 'error' there; this slice drops — * SEMANTICS.md documents the bound). */ static void scr_net_sock_write_raw(ScrNetSocket *s, const char *data, size_t len) { + if (s->adopted_by != NULL) { + /* Node writes these bytes to the wire as they are, between the TLS + * records of the socket that took over (the peer then fails the + * session). Bytes the kernel cannot take now end that socket. */ + ScrNetSocket *t = s->adopted_by; + while (len > 0 && t->fd >= 0) { + ssize_t sent = scr_net_send(t->fd, data, len); + if (sent < 0 && errno == EINTR) continue; + if (sent <= 0) { + scr_net_sock_destroy(t); + break; + } + data += sent; + len -= (size_t)sent; + } + return; + } /* A socket whose dial hasn't STARTED yet (the agent's deferred dial, * the caller-lookup wait) has no fd but is logically connecting — * bytes buffer and flush at establishment like any mid-connect write. */ @@ -1490,6 +1526,12 @@ static void scr_net_sock_tunnel_pump(ScrNetSocket *s) { } scr_net_sock_update_write(s); if (s->fd < 0 || t->out_off < t->out_len) return; + if (t->send_only) { + scr_net_tunnel_free(t); + s->tunnel = NULL; + scr_net_sock_connected(s); + return; + } for (;;) { char buf[4096]; ssize_t n = read(s->fd, buf, sizeof buf); @@ -1592,6 +1634,10 @@ static void scr_net_sock_transport_pump(ScrNetSocket *s) { if (scr_exc_pending()) return; scr_net_fire0_this(&s->ready_ls, s, SCR_DYNH_NET_SOCKET); if (scr_exc_pending()) return; + if (s->tops->after_established) { + s->tops->after_established(s->tctx); + if (scr_exc_pending()) return; + } scr_net_sock_flush(s); scr_net_sock_update_read(s); scr_net_sock_update_write(s); @@ -2250,6 +2296,8 @@ ScrStr *scr_net_blocking_lookup(ScrStr *host /*borrowed*/) { * autoselect), and any other name is resolved when the dial starts. */ static void scr_net_sock_set_peer(ScrNetSocket *s, ScrStr *host /*borrowed, nullable*/) { const char *h = host && host->len > 0 ? host->data : "localhost"; + scr_str_release(s->host_name); + s->host_name = scr_str_new(h, strlen(h)); if (strcmp(h, "localhost") == 0) { h = "127.0.0.1"; if (scr_net_lookup_ops != NULL) scr_net_lookup_ops->record(s, "localhost", 9, h, 4, NULL); @@ -3024,6 +3072,10 @@ void scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) { } void scr_net_sock_end(ScrNetSocket *s) { + if (s->adopted_by != NULL) { + scr_net_sock_end(s->adopted_by); + return; + } if (s->fd < 0 || s->wr_ending) return; s->wr_ending = true; scr_net_sock_maybe_finish_write(s); @@ -3136,7 +3188,10 @@ void scr_net_sock_on_write_flush(ScrNetSocket *s, ScrClosure *cb /*moves*/) { double scr_net_sock_bytes_written(ScrNetSocket *s) { return (double)s->bytes_written; } /* socket.readable: true until the read half is done (peer FIN / destroy). */ -bool scr_net_sock_readable(ScrNetSocket *s) { return s->fd >= 0 && !s->rd_eof; } +bool scr_net_sock_readable(ScrNetSocket *s) { + if (s->adopted_by != NULL) return scr_net_sock_readable(s->adopted_by); + return s->fd >= 0 && !s->rd_eof; +} /* socket.setTimeout(ms): ms > 0 arms the idle timer NOW (Node starts the * clock at the call — connecting time counts), ms <= 0 disables. */ @@ -3206,6 +3261,7 @@ ScrStr *scr_net_sock_remote_family(ScrNetSocket *s) { /* socket.localAddress / localPort / localFamily: read from the open * socket (undefined while connecting and after close, as in Node). */ static bool scr_net_sock_local(ScrNetSocket *s, char *addr, size_t cap, int *port, bool *v6) { + if (s->adopted_by != NULL) return scr_net_sock_local(s->adopted_by, addr, cap, port, v6); if (s->fd < 0 || s->connecting || s->dial_deferred || s->lookup_wait) return false; struct sockaddr_storage ss; socklen_t len = sizeof ss; @@ -4652,6 +4708,10 @@ bool scr_net_is_ipv4(const ScrDyn *input) { return scr_net_is_ip(input) == 4; } bool scr_net_is_ipv6(const ScrDyn *input) { return scr_net_is_ip(input) == 6; } void scr_net_sock_destroy(ScrNetSocket *s) { + if (s->adopted_by != NULL) { + scr_net_sock_destroy(s->adopted_by); + return; + } if (s->fd < 0 && !s->in_registry) return; s->rd_eof = true; s->wr_done = true; @@ -4833,7 +4893,8 @@ void scr_net_sock_on_error(ScrNetSocket *s, ScrClosure *cb /*moves*/, ScrChildEr } void scr_net_sock_on_connect(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) { - if (s->close_emitted || (!s->connecting && s->fd < 0)) { + /* a socket that took over another's connection never emits 'connect' */ + if (s->close_emitted || (!s->connecting && s->fd < 0) || s->adopted_from != NULL) { scr_closure_release(cb); return; } @@ -4939,12 +5000,15 @@ void scr_net_sock_clear_native_reader(ScrNetSocket *s) { /* socket.destroyed — true once the fd is gone (destroy() or full close), * Node's flag for "this stream is done". */ -bool scr_net_sock_destroyed(ScrNetSocket *s) { return s->fd < 0; } +bool scr_net_sock_destroyed(ScrNetSocket *s) { + return s->adopted_by != NULL ? scr_net_sock_destroyed(s->adopted_by) : s->fd < 0; +} /* socket.writable — Node's stream flag: the write half is open (no end() * yet, no FIN sent, fd alive). Connecting sockets answer true — writes * queue, exactly Node. */ bool scr_net_sock_writable(ScrNetSocket *s) { + if (s->adopted_by != NULL) return scr_net_sock_writable(s->adopted_by); return s->fd >= 0 && !s->wr_ending && !s->wr_done; } @@ -4996,8 +5060,78 @@ void scr_net_sock_set_transport(ScrNetSocket *s, const ScrNetTransportOps *ops, scr_net_sock_update_write(s); } +/* A fresh socket takes over raw's open connection: the fd, the bytes it + * buffered either way and the peer identity. raw keeps its listeners and + * its server's accounting but does no I/O of its own; end() and destroy() + * on it act on the new socket, and its 'close' fires just before the new + * socket's. Bytes raw had not sent yet go out first. +1; the caller sets + * the transport, then starts it with scr_net_sock_adopt_start. NULL when + * raw has no open, idle connection to give (connecting, closed, already + * wrapped, or carrying a transport). */ +ScrNetSocket *scr_net_sock_adopt(ScrNetSocket *raw) { + if (raw->fd < 0 || scr_net_sock_connecting(raw) || raw->adopted_by != NULL || raw->tops != NULL) + return NULL; + ScrNetSocket *t = scr_net_sock_new(); + scr_str_release(scr_net_sock_remote_address(raw)); /* cache the peer while the fd is raw's */ + if (raw->remote_cache) { + t->remote_cache = scr_str_retain(raw->remote_cache); + t->remote_port_cache = raw->remote_port_cache; + t->remote_v6_cache = raw->remote_v6_cache; + } + memcpy(t->peer_ip, raw->peer_ip, sizeof t->peer_ip); + t->peer_port = raw->peer_port; + t->unrefd = raw->unrefd; + t->rd_eof = raw->rd_eof; + scr_net_sock_timer_cancel(raw); + if (raw->read_armed) scr_net_watch_read(raw->fd, raw, false); + if (raw->write_armed) scr_net_watch_write(raw->fd, raw, false); + raw->read_armed = raw->write_armed = false; + t->fd = raw->fd; + raw->fd = -1; + if (raw->rlen > raw->rhead) scr_net_sock_append_rbuf(t, raw->rbuf + raw->rhead, raw->rlen - raw->rhead); + raw->rhead = raw->rlen = 0; + while (raw->wlen > raw->whead) { + ssize_t sent = scr_net_send(t->fd, raw->wbuf + raw->whead, raw->wlen - raw->whead); + if (sent < 0 && errno == EINTR) continue; + if (sent <= 0) break; /* EAGAIN, or an error the transport will meet */ + raw->whead += (size_t)sent; + } + if (raw->wlen > raw->whead) { + ScrNetTunnel *tun = calloc(1, sizeof *tun); + if (!tun) scr_net_oom(); + tun->out_len = raw->wlen - raw->whead; + tun->out = malloc(tun->out_len); + if (!tun->out) scr_net_oom(); + memcpy(tun->out, raw->wbuf + raw->whead, tun->out_len); + tun->send_only = true; + t->tunnel = tun; + } + raw->whead = raw->wlen = 0; + raw->adopted_by = scr_net_sock_retain(t); + t->adopted_from = scr_net_sock_retain(raw); + scr_net_sock_register(t); + return t; +} + +/* Starts an adopted socket once its transport is set: the unsent bytes + * drain first, then the handshake runs (over any bytes raw had already + * read). */ +void scr_net_sock_adopt_start(ScrNetSocket *t) { + if (t->tunnel != NULL) { + scr_net_sock_update_write(t); + return; + } + /* the handshake starts from the poller (the socket is writable), never + * the constructing stack — Node's start is asynchronous too */ + scr_net_sock_transport_want_write(t); +} + int scr_net_sock_fd(ScrNetSocket *s) { return s->fd; } +/* The host a client socket was told to dial (Node's socket._host). +1; + * NULL on server sockets. */ +ScrStr *scr_net_sock_dial_name(ScrNetSocket *s) { return scr_str_retain(s->host_name); } + /* Drain up to n bytes of the receive buffer (peeked/unshifted bytes) — * consumers call this BEFORE the fd so re-queued bytes keep their * position in the stream. */ @@ -5050,6 +5184,32 @@ void scr_net_sock_replay(ScrNetSocket *s, const char *data, size_t len) { s->rcap = total; } +/* The peer closed before the handshake finished, on a socket whose + * stream the program already holds (new tls.TLSSocket): 'end', then + * 'close' without an error, as Node reports it. */ +void scr_net_sock_transport_eof(ScrNetSocket *s) { + scr_net_sock_eof(s); + if (s->fd >= 0) scr_net_sock_close_fd(s); +} + +/* A listener for the handshake's completion once TCP is up (a socket + * tls.connect({ socket }) took over is up from the start): it registers + * until the transport establishes. */ +void scr_net_sock_on_established(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) { + if (s->close_emitted || s->fd < 0 || s->tops == NULL || s->t_est) { + scr_closure_release(cb); + return; + } + scr_net_ls_add(&s->conn_ls, cb, NULL, once); +} + +/* scr_net_sock_transport_error with the error's code given (static + * storage), for messages whose text does not name it. */ +void scr_net_sock_transport_error_code(ScrNetSocket *s, const char *msg, const char *code) { + if (!s->pending_err) s->pending_code = code; + scr_net_sock_transport_error(s, msg); +} + void scr_net_sock_transport_error(ScrNetSocket *s, const char *msg) { if (msg != NULL) { if (!s->pending_err) s->pending_err = scr_str_new(msg, strlen(msg)); @@ -5141,6 +5301,71 @@ static void scr_net_server_settle(ScrNetServer *srv) { scr_net_server_unregister(srv); } +/* Fires a closed socket's 'close' and settles it: its server's count + * drains, the protocol layer hears it, listeners drop and it leaves the + * registry. A socket that took over another's connection settles that one + * first, so the wrapped socket's 'close' comes just before its own. False + * with an exception pending. */ +static bool scr_net_sock_settle(ScrNetSocket *sock) { + ScrNetSocket *raw = sock->adopted_from; + if (raw != NULL) { + sock->adopted_from = NULL; + if (raw->adopted_by == sock) { + raw->adopted_by = NULL; + scr_net_sock_release(sock); + } + bool ok = raw->close_emitted || scr_net_sock_settle(raw); + scr_net_sock_release(raw); + if (!ok) return false; + } + sock->close_emitted = true; + sock->emit_close = false; + /* The owning server's count drains FIRST, and a now-drained closing + * server settles before this socket's own 'close' fires — Node's + * observed order (differential: the net-echo fixture). */ + if (sock->server) { + ScrNetServer *srv = sock->server; + sock->server = NULL; + srv->nconns--; + if (srv->closing && srv->nconns == 0 && !srv->close_emitted) { + scr_net_server_settle(srv); + } + scr_net_server_release(srv); + if (scr_exc_pending()) return false; + } + if (sock->native_closed) { + sock->native_closed(sock->native_ctx); + if (scr_exc_pending()) return false; + } + { + ScrDyn *had_error = scr_dyn_new_bool(sock->had_error); + scr_net_fire_arg_this(&sock->close_ls, had_error, sock, SCR_DYNH_NET_SOCKET); + scr_dyn_release(had_error); + } + /* settle: listeners drop (cycle story) */ + scr_net_ls_drop(&sock->data_ls); + scr_net_ls_drop(&sock->end_ls); + scr_net_ls_drop(&sock->close_ls); + scr_net_ls_drop(&sock->err_ls); + scr_net_ls_drop(&sock->conn_ls); + scr_net_ls_drop(&sock->timeout_ls); + scr_net_ls_drop(&sock->readable_ls); + scr_net_ls_drop(&sock->finish_ls); + scr_net_ls_drop(&sock->wcb_ls); + scr_net_ls_drop(&sock->ready_ls); + scr_net_ls_drop(&sock->lookup_ls); + if (sock->pipe_dst) { + scr_net_sock_release(sock->pipe_dst); + sock->pipe_dst = NULL; + } + if (sock->conn_pending) { + scr_net_server_release(sock->conn_pending); + sock->conn_pending = NULL; + } + scr_net_sock_unregister(sock); + return !scr_exc_pending(); +} + /* Sockets first (a dying connection may drain its server), servers after. * Every fire checks for a pending exception and bails — the loop surfaces * it as an uncaught throw. */ @@ -5200,7 +5425,19 @@ static void scr_net_sweep(void) { * listeners — and the unhandled-'error' exit */ bool consumed = false; if (sock->native_err) consumed = sock->native_err(sock->native_ctx, msg); - if (!consumed && !scr_exc_pending()) scr_net_fire_err_this(&sock->err_ls, msg, sock, SCR_DYNH_NET_SOCKET); + if (!consumed && !scr_exc_pending()) { + ScrError *coded = NULL; + ScrError *saved = scr_event_error_payload; + if (sock->pending_code != NULL) { + coded = scr_error_new(SCR_ERR_ERROR, msg); + coded->code = scr_str_new(sock->pending_code, strlen(sock->pending_code)); + scr_event_error_payload = coded; + } + scr_net_fire_err_this(&sock->err_ls, msg, sock, SCR_DYNH_NET_SOCKET); + scr_event_error_payload = saved; + if (coded != NULL) scr_error_release(coded); + } + sock->pending_code = NULL; scr_str_release(msg); if (scr_exc_pending()) { scr_net_sock_release(sock); @@ -5208,58 +5445,7 @@ static void scr_net_sweep(void) { } } if (sock->emit_close && !sock->close_emitted && sock->fd < 0) { - sock->close_emitted = true; - sock->emit_close = false; - /* The owning server's count drains FIRST, and a now-drained closing - * server settles before this socket's own 'close' fires — Node's - * observed order (differential: the net-echo fixture). */ - if (sock->server) { - ScrNetServer *srv = sock->server; - sock->server = NULL; - srv->nconns--; - if (srv->closing && srv->nconns == 0 && !srv->close_emitted) { - scr_net_server_settle(srv); - } - scr_net_server_release(srv); - if (scr_exc_pending()) { - scr_net_sock_release(sock); - return; - } - } - if (sock->native_closed) { - sock->native_closed(sock->native_ctx); - if (scr_exc_pending()) { - scr_net_sock_release(sock); - return; - } - } - { - ScrDyn *had_error = scr_dyn_new_bool(sock->had_error); - scr_net_fire_arg_this(&sock->close_ls, had_error, sock, SCR_DYNH_NET_SOCKET); - scr_dyn_release(had_error); - } - /* settle: listeners drop (cycle story) */ - scr_net_ls_drop(&sock->data_ls); - scr_net_ls_drop(&sock->end_ls); - scr_net_ls_drop(&sock->close_ls); - scr_net_ls_drop(&sock->err_ls); - scr_net_ls_drop(&sock->conn_ls); - scr_net_ls_drop(&sock->timeout_ls); - scr_net_ls_drop(&sock->readable_ls); - scr_net_ls_drop(&sock->finish_ls); - scr_net_ls_drop(&sock->wcb_ls); - scr_net_ls_drop(&sock->ready_ls); - scr_net_ls_drop(&sock->lookup_ls); - if (sock->pipe_dst) { - scr_net_sock_release(sock->pipe_dst); - sock->pipe_dst = NULL; - } - if (sock->conn_pending) { - scr_net_server_release(sock->conn_pending); - sock->conn_pending = NULL; - } - scr_net_sock_unregister(sock); - if (scr_exc_pending()) { + if (!scr_net_sock_settle(sock)) { scr_net_sock_release(sock); return; } @@ -5504,6 +5690,17 @@ static void scr_net_cleanup_atexit(void) { scr_net_server_release(s->server); s->server = NULL; } + /* a wrapped socket and the socket that took it over hold each other */ + if (s->adopted_by) { + ScrNetSocket *t = s->adopted_by; + s->adopted_by = NULL; + scr_net_sock_release(t); + } + if (s->adopted_from) { + ScrNetSocket *raw = s->adopted_from; + s->adopted_from = NULL; + scr_net_sock_release(raw); + } scr_net_sock_unregister(s); } while (scr_net_servers) { diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index 33638f699c..8f26bbf4d5 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -351,8 +351,19 @@ typedef struct ScrNetTransportOps { bool (*pending)(void *tctx); void (*shutdown_write)(void *tctx); void (*free)(void *tctx); + /* nullable: runs after the established socket's 'connect' listeners + * (the TLS 'secure' event, which Node emits after secureConnect) */ + void (*after_established)(void *tctx); } ScrNetTransportOps; void scr_net_sock_set_transport(ScrNetSocket *s, const ScrNetTransportOps *ops, void *tctx); +/* new tls.TLSSocket(socket) and tls.connect({ socket }): a fresh socket + * takes over an open connection (fd, buffered bytes, peer identity); the + * old socket stays open without I/O, forwards end() and destroy(), and + * fires 'close' just before the new one. +1, or NULL when the socket has + * no open, idle connection to give. Set the transport on the result, then + * call scr_net_sock_adopt_start. */ +ScrNetSocket *scr_net_sock_adopt(ScrNetSocket *raw); +void scr_net_sock_adopt_start(ScrNetSocket *t); /* Routes a client socket through an HTTP CONNECT proxy. Call it right * after the dial (scr_net_connect to the proxy) and before any transport * is set: once TCP is up, `request` (the CONNECT head, copied) is sent @@ -366,6 +377,9 @@ void scr_net_sock_set_tunnel(ScrNetSocket *s, const char *request, size_t len); int scr_net_tunnel_status(const char *head, size_t len); void scr_net_tunnel_failure(int status, char *msg, size_t cap); int scr_net_sock_fd(ScrNetSocket *s); +/* The host a client socket was told to dial (Node's socket._host). +1; + * NULL on server sockets. */ +ScrStr *scr_net_sock_dial_name(ScrNetSocket *s); /* Drain up to n bytes of the socket's receive buffer (peeked/unshifted * bytes — the demux path); consumers call it before the fd. */ size_t scr_net_sock_take_buffered(ScrNetSocket *s, char *buf, size_t n); @@ -385,6 +399,16 @@ void scr_net_sock_replay(ScrNetSocket *s, const char *data, size_t len); * 'error' + teardown to the sweep; NULL tears down without an 'error' * event, Node's default for an unhandled server-side handshake failure. */ void scr_net_sock_transport_error(ScrNetSocket *s, const char *msg); +/* The same with the error's code given (static storage), for messages + * whose text does not name it. */ +void scr_net_sock_transport_error_code(ScrNetSocket *s, const char *msg, const char *code); +/* The peer closed before the handshake finished on a wrapped socket: + * 'end', then 'close' without an error. */ +void scr_net_sock_transport_eof(ScrNetSocket *s); +/* Registers a listener that fires when the transport establishes, once + * TCP is up (a socket that took over an open connection is up from the + * start). */ +void scr_net_sock_on_established(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once); /* Defer the server's 'connection' listeners to handshake completion — * tls.createServer's handler is the 'secureConnection' event, fired with * the socket only once the TLS layer stands (Node's timing). */ diff --git a/packages/runtime/src/scr_tls.c b/packages/runtime/src/scr_tls.c index b4d125cb28..6fa1f169d7 100644 --- a/packages/runtime/src/scr_tls.c +++ b/packages/runtime/src/scr_tls.c @@ -368,6 +368,10 @@ typedef struct ScrTlsSrv { /* maxVersion TLSv1.2: the ClientHello is pre-parsed so a hello that * offers only later versions gets OpenSSL's protocol_version alert. */ bool max_tls12; + /* new tls.TLSSocket(socket, { secureContext }): the context whose + * certificate and key this config serves (+1; NULL when cert/key are the + * config's own) */ + ScrSecureCtx *ctx; } ScrTlsSrv; /* The ALPN answers (static storage — mbedTLS keeps the pointers). Plain @@ -405,6 +409,28 @@ static ScrTlsSrv *scr_tls_srv_new(const char *cert, size_t cert_len, const char return s; } +/* A server config that serves a SecureContext's certificate and key. */ +static ScrTlsSrv *scr_tls_srv_from_ctx(ScrSecureCtx *ctx /*borrowed*/) { + scr_tls_rng_init(); + ScrTlsSrv *s = calloc(1, sizeof *s); + if (!s) scr_tls_oom(); + s->rc = 1; + mbedtls_x509_crt_init(&s->cert); + mbedtls_pk_init(&s->pk); + mbedtls_ssl_config_init(&s->conf); + if (mbedtls_ssl_config_defaults(&s->conf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT) != 0) { + scr_tls_oom(); + } + mbedtls_ssl_conf_rng(&s->conf, mbedtls_ctr_drbg_random, &scr_tls_drbg); + mbedtls_ssl_conf_authmode(&s->conf, MBEDTLS_SSL_VERIFY_NONE); + s->ctx = scr_secure_ctx_retain(ctx); + if (mbedtls_ssl_conf_own_cert(&s->conf, &ctx->cert, &ctx->pk) != 0) { + scr_tls_bad_material("certificate/key pair", 0); + } + return s; +} + static ScrTlsSrv *scr_tls_srv_retain(ScrTlsSrv *s) { s->rc++; return s; @@ -419,6 +445,7 @@ static void scr_tls_srv_release(ScrTlsSrv *s) { mbedtls_x509_crt_free(&s->cert); mbedtls_pk_free(&s->pk); scr_tls_alpn_free(s->alpn); + scr_secure_ctx_release(s->ctx); free(s); } @@ -629,6 +656,14 @@ typedef struct ScrTlsSock { unsigned char *hello; /* the raw ClientHello flight, replayed at DONE */ size_t hello_len, hello_cap; ScrSecureCtx *sni_ctx; /* owned; the answer (NULL = default cert) */ + /* new tls.TLSSocket(socket, { isServer: true }): the program holds this + * socket before the handshake, so a failed handshake is its 'error' + * (tls.createServer's sockets fail without one) */ + bool wrapped; + unsigned char head[5]; /* the first bytes the peer sent (the error's wording) */ + size_t head_len; + /* the 'secure' event's listeners */ + ScrNetLs secure_ls; } ScrTlsSock; /* checkServerIdentity for an IP literal: the address must be one of the @@ -703,6 +738,16 @@ static int scr_tls_bio_send(void *ctx, const unsigned char *buf, size_t len) { return MBEDTLS_ERR_NET_SEND_FAILED; } +/* Keeps the first bytes a wrapped socket's peer sends: a handshake that + * fails on them reports what they looked like, as OpenSSL does. */ +static void scr_tls_note_head(ScrTlsSock *t, const unsigned char *buf, size_t n) { + if (!t->wrapped || t->head_len == sizeof t->head) return; + size_t take = sizeof t->head - t->head_len; + if (take > n) take = n; + memcpy(t->head + t->head_len, buf, take); + t->head_len += take; +} + static int scr_tls_bio_recv(void *ctx, unsigned char *buf, size_t len) { ScrTlsSock *t = (ScrTlsSock *)ctx; int fd = scr_net_sock_fd(t->sock); @@ -710,8 +755,12 @@ static int scr_tls_bio_recv(void *ctx, unsigned char *buf, size_t len) { /* bytes a consumer peeked and unshifted (the demux path) re-enter the * stream here, ahead of the kernel's */ size_t took = scr_net_sock_take_buffered(t->sock, (char *)buf, len); - if (took > 0) return (int)took; + if (took > 0) { + scr_tls_note_head(t, buf, took); + return (int)took; + } ssize_t n = read(fd, buf, len); + if (n > 0) scr_tls_note_head(t, buf, (size_t)n); if (n >= 0) return (int)n; /* 0 = EOF; mbedTLS maps it to CONN_EOF */ if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) { return MBEDTLS_ERR_SSL_WANT_READ; @@ -1026,6 +1075,43 @@ void scr_tls_sni_answer(ScrClosure *self, bool has_err, ScrSecureCtx *ctx /*move scr_net_sock_release(sock); } +/* A wrapped server socket's failed handshake, reported as Node reports it + * on the TLSSocket: a peer that closed first ends the stream, a reset is + * ECONNRESET, and bytes that are not a TLS record name what they were + * (OpenSSL's reasons without the per-process prefix and source location, + * SEMANTICS.md divergence 55). */ +static void scr_tls_wrapped_failure(ScrTlsSock *t, int r) { + if (r == MBEDTLS_ERR_SSL_CONN_EOF) { + scr_net_sock_transport_eof(t->sock); + return; + } + if (r == MBEDTLS_ERR_NET_CONN_RESET) { + scr_net_sock_transport_error(t->sock, "read ECONNRESET"); + return; + } + const unsigned char *h = t->head; + bool record = t->head_len >= 3 && h[0] >= 20 && h[0] <= 23 && h[1] == 3; + if (t->head_len >= 5 && !record) { + static const char *const http[] = { "GET ", "POST ", "HEAD ", "PATCH", "OPTIO", + "DELET", "TRACE", "PUT ", NULL }; + const char *reason = "error:0A00010B:SSL routines:tls_validate_record_header:wrong version number"; + const char *code = "ERR_SSL_WRONG_VERSION_NUMBER"; + for (size_t i = 0; http[i] != NULL; i++) { + if (memcmp(h, http[i], strlen(http[i])) == 0) { + reason = "error:0A00009C:SSL routines:tls_validate_record_header:http request"; + code = "ERR_SSL_HTTP_REQUEST"; + } + } + if (memcmp(h, "CONNE", 5) == 0) { + reason = "error:0A00009B:SSL routines:tls_validate_record_header:https proxy request"; + code = "ERR_SSL_HTTPS_PROXY_REQUEST"; + } + scr_net_sock_transport_error_code(t->sock, reason, code); + return; + } + scr_net_sock_transport_error(t->sock, "server TLS handshake failed"); +} + static int scr_tls_handshake(void *tctx) { ScrTlsSock *t = (ScrTlsSock *)tctx; if (t->cli != NULL && t->cli->no_protocols) { @@ -1046,6 +1132,10 @@ static int scr_tls_handshake(void *tctx) { scr_tls_current = NULL; if (r == 0) return 1; if (r == MBEDTLS_ERR_SSL_WANT_READ || r == MBEDTLS_ERR_SSL_WANT_WRITE) return 0; + if (t->srv != NULL && t->wrapped) { + scr_tls_wrapped_failure(t, r); + return -1; + } if (t->srv != NULL) { /* Node's server default: 'tlsClientError' with no listener destroys * the socket without an error event — exactly what portless (no @@ -1133,11 +1223,20 @@ static void scr_tls_free(void *tctx) { if (t->srv) scr_tls_srv_release(t->srv); if (t->cli) scr_tls_cli_free(t->cli); scr_net_ls_drop(&t->session_ls); + scr_net_ls_drop(&t->secure_ls); free(t->hello); scr_secure_ctx_release(t->sni_ctx); free(t); } +/* The 'secure' event: after a client's secureConnect listeners, as Node + * emits it. */ +static void scr_tls_after_established(void *tctx) { + ScrTlsSock *t = (ScrTlsSock *)tctx; + scr_net_fire0_this(&t->secure_ls, t->sock, SCR_DYNH_NET_SOCKET); + scr_net_ls_drop(&t->secure_ls); +} + static const ScrNetTransportOps SCR_TLS_OPS = { .xread = scr_tls_xread, .xwrite = scr_tls_xwrite, @@ -1146,6 +1245,7 @@ static const ScrNetTransportOps SCR_TLS_OPS = { .pending = scr_tls_pending, .shutdown_write = scr_tls_shutdown_write, .free = scr_tls_free, + .after_established = scr_tls_after_established, }; /* ── the server surface ──────────────────────────────────────────────── */ @@ -2088,6 +2188,101 @@ ScrNetServer *scr_https_create_server_dyn(const ScrDyn *opts /*borrowed*/, return s; } +/* ── new tls.TLSSocket(socket, options) ───────────────────────────────── */ + +/* The server-mode wrap: a fresh TLS socket takes over raw's connection + * (scr_net_sock_adopt) and runs the server handshake over it. It serves + * `ctx` (options.secureContext, which the lowering passes apart from the + * record; borrowed, nullable) or the record's cert and key. The record + * reads at runtime like tls.createServer's: ALPNProtocols, minVersion and + * maxVersion apply, and options without a lowering throw the fence. Node + * never starts the handshake of a client-mode TLSSocket (no isServer), so + * that form throws the fence too. +1, or NULL with the exception pending. */ +static const char *const SCR_TLS_WRAP_FENCED_OPTIONS[] = { + "server", "session", "requestOCSP", "pskCallback", "enableTrace", NULL, +}; + +ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrowed*/, + ScrSecureCtx *ctx /*borrowed, nullable*/) { + static const char api[] = "new tls.TLSSocket"; + bool is_server = false; + const ScrDyn *record = opts != NULL && opts->kind == SCR_DYN_OBJ ? opts : NULL; + for (size_t i = 0; record != NULL && i < record->v.obj.len; i++) { + const ScrDynEntry *e = &record->v.obj.entries[i]; + if (e->value->kind == SCR_DYN_UNDEF) continue; + if (strcmp(e->key, "isServer") == 0) { + is_server = scr_tls_dyn_truthy(e->value); + continue; + } + if (strcmp(e->key, "secureContext") == 0) { + scr_tls_opt_fence(api, "secureContext", + "secureContext lowers as a member of an options object literal"); + return NULL; + } + for (size_t k = 0; SCR_TLS_WRAP_FENCED_OPTIONS[k] != NULL; k++) { + if (strcmp(e->key, SCR_TLS_WRAP_FENCED_OPTIONS[k]) == 0) { + scr_tls_opt_fence(api, e->key, + "isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members"); + return NULL; + } + } + } + if (!is_server) { + scr_tls_opt_fence(api, "isServer", + "Node starts no handshake for a client-mode TLSSocket; tls.connect({ socket }) runs the client handshake over an open socket"); + return NULL; + } + /* Node serves a secureContext as it is: the record's cert, key and + * other context options are not read then */ + int min_version = 12, max_version = 13; + ScrBytes *cert = NULL, *key = NULL; + if (ctx == NULL) { + if (!scr_tls_versions_dyn(record, &min_version, &max_version)) return NULL; + if (!scr_tls_srv_opts_walk_ex(record, api, &cert, &key, true)) return NULL; + } + char **alpn = NULL; + const ScrDyn *protocols = record != NULL ? scr_dyn_obj_get(record, "ALPNProtocols", 13) : NULL; + if (protocols != NULL && protocols->kind != SCR_DYN_UNDEF && !scr_tls_alpn_dyn(protocols, &alpn)) { + scr_bytes_release(cert); + scr_bytes_release(key); + return NULL; + } + ScrTlsSrv *srv = ctx != NULL ? scr_tls_srv_from_ctx(ctx) + : scr_tls_srv_new((const char *)cert->data, cert->len, + (const char *)key->data, key->len, false); + scr_bytes_release(cert); + scr_bytes_release(key); + srv->alpn = alpn; /* moves */ + if (alpn != NULL) mbedtls_ssl_conf_alpn_protocols(&srv->conf, (const char **)srv->alpn); + if (!scr_tls_conf_versions(&srv->conf, min_version, max_version)) { + scr_tls_srv_release(srv); + scr_tls_opt_fence(api, "maxVersion", + "a version range with neither TLSv1.2 nor TLSv1.3 has no lowering"); + return NULL; + } + srv->max_tls12 = max_version == 12; + ScrNetSocket *t = scr_net_sock_adopt(raw); + if (t == NULL) { + scr_tls_srv_release(srv); + static const char msg[] = + "new tls.TLSSocket over a socket that is not connected, or is already wrapped, is not supported yet"; + scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1); + return NULL; + } + ScrTlsSock *ts = calloc(1, sizeof *ts); + if (!ts) scr_tls_oom(); + mbedtls_ssl_init(&ts->ssl); + if (mbedtls_ssl_setup(&ts->ssl, &srv->conf) != 0) scr_tls_oom(); + ts->sock = t; + ts->srv = srv; /* moves */ + ts->wrapped = true; + if (srv->max_tls12) ts->sni_state = SCR_SNI_COLLECT; /* pre-parse the hello */ + mbedtls_ssl_set_bio(&ts->ssl, ts, scr_tls_bio_send, scr_tls_bio_recv, NULL); + scr_net_sock_set_transport(t, &SCR_TLS_OPS, ts); + scr_net_sock_adopt_start(t); + return t; +} + /* ── tls.connect (the TLS client socket) ──────────────────────────────── */ /* Node's inspect of a Buffer inside ERR_INVALID_ARG_VALUE's message: @@ -2239,7 +2434,7 @@ static const char *const SCR_TLS_CONNECT_FENCED_OPTIONS[] = { "localAddress", "localPort", "lookup", "minDHSize", "noDelay", "onread", "passphrase", "path", "pfx", "privateKeyEngine", "privateKeyIdentifier", "pskCallback", "readable", "secureContext", "secureOptions", "secureProtocol", "session", - "sessionIdContext", "sigalgs", "signal", "socket", "ticketKeys", + "sessionIdContext", "sigalgs", "signal", "ticketKeys", "writable", NULL, }; @@ -2255,6 +2450,7 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable const ScrDyn *alpn_value = NULL; const ScrDyn *timeout_value = NULL; int min_version = 12, max_version = 13; + ScrNetSocket *wrap = NULL; /* options.socket: the open socket to take over */ bool ok = true; if (opts != NULL && opts->kind != SCR_DYN_UNDEF && opts->kind != SCR_DYN_NULL) { if (opts->kind != SCR_DYN_OBJ) { @@ -2322,6 +2518,15 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable timeout_value = scr_tls_dyn_truthy(v) ? v : NULL; continue; } + if (strcmp(e->key, "socket") == 0) { + if (v->kind == SCR_DYN_HANDLE && v->v.handle.tag == SCR_DYNH_NET_SOCKET) { + wrap = (ScrNetSocket *)v->v.handle.ptr; + } else { + scr_tls_opt_fence("tls.connect", "socket", "a net.Socket is the implemented socket value"); + ok = false; + } + continue; + } if (strcmp(e->key, "servername") == 0) { if (v->kind == SCR_DYN_STR) servername = v->v.str; else { @@ -2333,7 +2538,7 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable for (size_t k = 0; SCR_TLS_CONNECT_FENCED_OPTIONS[k] != NULL; k++) { if (strcmp(e->key, SCR_TLS_CONNECT_FENCED_OPTIONS[k]) == 0) { scr_tls_opt_fence("tls.connect", e->key, - "port/host/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout are the implemented members of a runtime options record"); + "port/host/socket/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout are the implemented members of a runtime options record"); ok = false; break; } @@ -2361,7 +2566,7 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable timeout_ms = timeout_value->v.num; } } - if (ok && port < 0) { + if (ok && port < 0 && wrap == NULL) { scr_dyn_arg_type_fail("options.port", "of type number", scr_dyn_undefined()); ok = false; } @@ -2377,6 +2582,9 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable * name otherwise (Node's servername default); "" sends no SNI. */ ScrStr *verify_host = dial_host; ScrStr *localhost_fallback = NULL; + /* over options.socket, the host that socket dialed (Node's socket._host) */ + ScrStr *socket_host = verify_host == NULL && wrap != NULL ? scr_net_sock_dial_name(wrap) : NULL; + if (socket_host != NULL) verify_host = socket_host; if (verify_host == NULL) { localhost_fallback = scr_str_new("localhost", 9); verify_host = localhost_fallback; @@ -2384,9 +2592,25 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable /* Resolve for the dial only (the http client's rule): verify_host above * already holds the NAME, which is what SNI and certificate verification * must see. A null dial_host stays null — scr_net_connect's own arm. */ - ScrStr *dial_addr = dial_host != NULL ? scr_net_blocking_lookup(dial_host) : NULL; - ScrNetSocket *s = scr_net_connect(port, dial_addr, cb); /* cb fires post-handshake — secureConnect */ - if (dial_addr) scr_str_release(dial_addr); + ScrNetSocket *s; + if (wrap != NULL) { + s = scr_net_sock_adopt(wrap); + if (s == NULL) { + scr_tls_alpn_free(alpn); + scr_bytes_release(ca); + scr_closure_release(cb); + scr_str_release(localhost_fallback); + scr_str_release(socket_host); + static const char msg[] = + "tls.connect over a socket that is not connected, or is already wrapped, is not supported yet"; + scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1); + return NULL; + } + } else { + ScrStr *dial_addr = dial_host != NULL ? scr_net_blocking_lookup(dial_host) : NULL; + s = scr_net_connect(port, dial_addr, cb); /* cb fires post-handshake — secureConnect */ + if (dial_addr) scr_str_release(dial_addr); + } ScrTlsCli *cli = scr_tls_cli_new2(verify_host, reject, ca != NULL ? (const char *)ca->data : NULL, ca != NULL ? ca->len : 0, !reject); @@ -2397,8 +2621,13 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable } cli->no_protocols = !scr_tls_conf_versions(&cli->conf, min_version, max_version); scr_tls_cli_wrap(s, cli); + if (wrap != NULL) { + if (cb != NULL) scr_net_sock_on_established(s, cb, true); /* secureConnect */ + scr_net_sock_adopt_start(s); + } if (timeout_ms > 0) scr_net_sock_set_timeout(s, timeout_ms); scr_str_release(localhost_fallback); + scr_str_release(socket_host); scr_bytes_release(ca); return s; } @@ -2610,12 +2839,39 @@ ScrDyn *scr_tls_sock_alpn_protocol(ScrNetSocket *s) { return d; } +/* secureConnect: a client socket's 'connect' list fires at establishment + * (scr_net.c's transport pump), Node's secureConnect timing. Once TCP is up + * (a socket tls.connect({ socket }) took over is up from the start) the + * listener waits for the handshake instead. Server-side sockets never emit + * it. */ +static void scr_tls_add_secure_connect(ScrNetSocket *s, ScrTlsSock *t, ScrClosure *cb, bool once) { + if (t->srv != NULL) { + scr_closure_release(cb); + return; + } + if (scr_net_sock_connecting(s)) scr_net_sock_on_connect(s, cb, once); + else scr_net_sock_on_established(s, cb, once); +} + void scr_tls_sock_on_secure_connect(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) { - if (scr_tls_sock_of(s) == NULL) { + ScrTlsSock *t = scr_tls_sock_of(s); + if (t == NULL) { scr_closure_release(cb); /* a plain socket never fires it, like Node */ return; } - scr_net_sock_on_connect(s, cb, once); /* fires at establishment — secureConnect timing */ + scr_tls_add_secure_connect(s, t, cb, once); +} + +/* The 'secure' event: the handshake completed (after secureConnect on a + * client). A plain socket never emits it, and a registration after the + * handshake comes too late, as in Node. */ +void scr_tls_sock_on_secure(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) { + ScrTlsSock *t = scr_tls_sock_of(s); + if (t == NULL || t->established) { + scr_closure_release(cb); + return; + } + scr_net_ls_add(&t->secure_ls, cb, NULL, once); } void scr_tls_sock_on_session(ScrNetSocket *s, ScrClosure *cb /*moves*/, void *fn, bool once) { @@ -2660,9 +2916,11 @@ static bool scr_tls_dynh_on(ScrNetSocket *s, const char *event, const ScrDyn *cb ScrTlsSock *t = scr_tls_sock_of(s); if (t == NULL) return false; if (strcmp(event, "secureConnect") == 0) { - /* a TLS socket's 'connect' list fires at establishment — exactly - * Node's secureConnect timing (scr_net.c's transport pump) */ - scr_net_sock_on_connect(s, scr_dyn_listener_closure0(cb), once); + scr_tls_add_secure_connect(s, t, scr_dyn_listener_closure0(cb), once); + return true; + } + if (strcmp(event, "secure") == 0) { + scr_tls_sock_on_secure(s, scr_dyn_listener_closure0(cb), once); return true; } if (strcmp(event, "session") == 0) { diff --git a/packages/runtime/src/scr_tls.h b/packages/runtime/src/scr_tls.h index fcb221b428..cc8e3336e7 100644 --- a/packages/runtime/src/scr_tls.h +++ b/packages/runtime/src/scr_tls.h @@ -62,6 +62,10 @@ ScrDyn *scr_tls_sock_get_cipher(ScrNetSocket *s); /* +1: object, undefined or ScrDyn *scr_tls_sock_alpn_protocol(ScrNetSocket *s); /* +1: string, false or null */ void scr_tls_sock_on_secure_connect(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once); void scr_tls_sock_on_session(ScrNetSocket *s, ScrClosure *cb /*moves*/, void *fn, bool once); +/* The 'secure' event: fires when the handshake completes (after the + * secureConnect listeners on a client); released unread on a plain socket + * or after the handshake. */ +void scr_tls_sock_on_secure(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once); /* ── h2 over TLS (scr_tls.c ↔ scr_http2.c): the ALPN=h2 server wrap and * the https-authority client wrap. scr_http2.c calls both directly — @@ -93,6 +97,11 @@ ScrSecureCtx *scr_tls_create_secure_context(const char *cert, size_t cert_len, c * validations first, then the pem walk (+1, or NULL with the exception * pending). Borrowed. */ ScrSecureCtx *scr_tls_create_secure_context_dyn(const ScrDyn *opts); +/* new tls.TLSSocket(socket, options) in server mode: a fresh TLS socket + * takes over socket's connection and runs the server handshake, serving + * ctx (options.secureContext; borrowed, nullable) or the options record's + * cert and key. +1, or NULL with the exception pending. */ +ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrowed*/, ScrSecureCtx *ctx /*borrowed, nullable*/); /* tls.getCACertificates(type): validateString + the documented name set, * then the compiler-rendered fence — always leaves an exception pending. */ void scr_tls_ca_certs_chk(const ScrDyn *type, const ScrStr *fence); diff --git a/tests/corpus/7087-tls-socket-wrap.ts b/tests/corpus/7087-tls-socket-wrap.ts new file mode 100644 index 0000000000..d1357a98ea --- /dev/null +++ b/tests/corpus/7087-tls-socket-wrap.ts @@ -0,0 +1,104 @@ +// new tls.TLSSocket(socket, { isServer: true, ... }) over a net server's +// connection: the wrap is a new socket that runs the server handshake, with +// cert/key or a secureContext and ALPNProtocols. The wrapped socket stays +// open without I/O, destroy() on it closes both, and its 'close' fires just +// before the TLS socket's. A peer that closes, resets or sends bytes that +// are not TLS fails the handshake with Node's events and codes. +import { readFileSync } from "node:fs"; +import * as net from "node:net"; +import * as tls from "node:tls"; + +const certs = "tests/fixtures/server/certs"; +const ca = readFileSync(`${certs}/ca.pem`); +const cert = readFileSync(`${certs}/localhost.pem`); +const key = readFileSync(`${certs}/localhost-key.pem`); +const context = tls.createSecureContext({ cert, key }); + +// cert/key and ALPN: the events, the state of both sockets, and the data. +function certKey(next: () => void): void { + const server = net.createServer((raw) => { + raw.on("close", (hadError: boolean) => console.log("raw close", hadError, raw.destroyed)); + const s = new tls.TLSSocket(raw, { isServer: true, cert, key, ALPNProtocols: ["h2", "http/1.1"] }); + console.log("after wrap", raw.destroyed, raw.readyState, raw.writable, s === raw); + console.log("before handshake", s.destroyed, s.encrypted, s.alpnProtocol); + console.log("same peer", s.remoteAddress === raw.remoteAddress, s.remotePort === raw.remotePort); + s.on("connect", () => console.log("never: connect")); + s.on("secureConnect", () => console.log("never: secureConnect")); + s.on("secure", () => { + console.log("secure", s.getProtocol(), s.alpnProtocol, s.authorized, s.authorizationError); + }); + s.on("data", (chunk: Buffer) => { + console.log("server data", chunk.toString()); + s.end("pong"); + }); + s.on("end", () => console.log("server end")); + s.on("close", (hadError: boolean) => { + console.log("tls close", hadError, raw.destroyed, s.destroyed); + server.close(next); + }); + }); + server.listen(0, () => { + const port = (server.address() as net.AddressInfo).port; + const c = tls.connect({ port, host: "localhost", ca, ALPNProtocols: ["http/1.1"] }, () => { + console.log("client secureConnect", c.authorized, c.alpnProtocol); + c.write("ping"); + }); + c.on("secure", () => console.log("client secure")); + c.on("data", (chunk: Buffer) => console.log("client data", chunk.toString())); + c.on("close", () => console.log("client close")); + }); +} + +// secureContext, and destroy() on the wrapped socket. +function secureContext(next: () => void): void { + const server = net.createServer((raw) => { + raw.on("close", (hadError: boolean) => console.log("raw close", hadError)); + const s = new tls.TLSSocket(raw, { isServer: true, secureContext: context }); + s.on("secure", () => { + console.log("context secure", s.alpnProtocol); + raw.destroy(); + }); + s.on("close", (hadError: boolean) => { + console.log("tls close", hadError, s.destroyed); + server.close(next); + }); + }); + server.listen(0, () => { + const port = (server.address() as net.AddressInfo).port; + const c = tls.connect({ port, host: "localhost", ca }, () => console.log("client secureConnect")); + c.on("error", (err: Error & { code?: string }) => console.log("client error", err.code)); + c.on("close", () => console.log("client close")); + }); +} + +// Handshakes the peer fails. +function failure(mode: string, next: () => void): void { + const server = net.createServer((raw) => { + const s = new tls.TLSSocket(raw, { isServer: true, cert, key }); + s.on("secure", () => console.log("never: secure")); + s.on("error", (err: Error & { code?: string }) => console.log(mode, "error", err.code)); + s.on("end", () => console.log(mode, "end")); + s.on("close", (hadError: boolean) => { + console.log(mode, "close", hadError); + server.close(next); + }); + }); + server.listen(0, () => { + const port = (server.address() as net.AddressInfo).port; + const c = net.connect(port, "localhost", () => { + if (mode === "close") c.end(); + else if (mode === "garbage") c.end("hello there, this is not tls\r\n"); + else if (mode === "http") c.end("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"); + else c.resetAndDestroy(); + }); + c.on("error", () => {}); + }); +} + +certKey(() => + secureContext(() => + failure("close", () => + failure("garbage", () => failure("http", () => failure("reset", () => console.log("done")))), + ), + ), +); diff --git a/tests/corpus/7088-tls-socket-connect-tunnel.ts b/tests/corpus/7088-tls-socket-connect-tunnel.ts new file mode 100644 index 0000000000..806575ddb5 --- /dev/null +++ b/tests/corpus/7088-tls-socket-connect-tunnel.ts @@ -0,0 +1,64 @@ +// An HTTPS-intercepting proxy: the http server answers CONNECT, then runs +// the TLS server side on the tunnel socket with new tls.TLSSocket and a +// secureContext; the client runs its handshake inside the tunnel with +// tls.connect({ socket }). Without a servername the client verifies against +// the host the tunnel socket dialed. +import { readFileSync } from "node:fs"; +import * as http from "node:http"; +import * as net from "node:net"; +import * as tls from "node:tls"; + +const certs = "tests/fixtures/server/certs"; +const ca = readFileSync(`${certs}/ca.pem`); +const context = tls.createSecureContext({ + cert: readFileSync(`${certs}/localhost.pem`), + key: readFileSync(`${certs}/localhost-key.pem`), +}); + +let tunnels = 0; +const proxy = http.createServer((_req, res) => { + res.end("plain"); +}); +proxy.on("connect", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => { + const id = ++tunnels; + console.log(id, "proxy connect", req.method, req.url, head.length); + socket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + const secure = new tls.TLSSocket(socket, { isServer: true, secureContext: context }); + secure.on("secure", () => console.log(id, "proxy secure", secure.getProtocol())); + secure.on("data", (chunk: Buffer) => { + console.log(id, "proxy got", chunk.toString()); + secure.end(`intercepted ${chunk.toString()}`); + }); + secure.on("close", () => { + console.log(id, "proxy tls close"); + if (id === 2) proxy.close(); + }); + socket.on("close", () => console.log(id, "tunnel close")); +}); + +function tunnel(port: number, servername: string | undefined, next: () => void): void { + const raw = net.connect(port, "localhost", () => { + raw.write("CONNECT example.test:443 HTTP/1.1\r\nHost: example.test:443\r\n\r\n"); + }); + raw.once("data", (chunk: Buffer) => { + console.log("client got", chunk.toString().split("\r\n")[0]); + const onSecure = (): void => { + console.log("client secureConnect", client.authorized); + client.write(servername ?? "no servername"); + }; + const client = + servername === undefined + ? tls.connect({ socket: raw, ca }, onSecure) + : tls.connect({ socket: raw, ca, servername }, onSecure); + client.on("data", (data: Buffer) => console.log("client data", data.toString())); + client.on("close", () => { + console.log("client close"); + next(); + }); + }); +} + +proxy.listen(0, () => { + const port = (proxy.address() as net.AddressInfo).port; + tunnel(port, "localhost", () => tunnel(port, undefined, () => console.log("done"))); +}); diff --git a/tests/corpus/7089-http-host-header-tunnels.ts b/tests/corpus/7089-http-host-header-tunnels.ts new file mode 100644 index 0000000000..bc982b2d10 --- /dev/null +++ b/tests/corpus/7089-http-host-header-tunnels.ts @@ -0,0 +1,31 @@ +// An HTTP/1.1 request without a Host header gets 400 Bad Request, except +// the requests that leave the parser first: an upgrade an 'upgrade' +// listener takes, and a CONNECT (destroyed here, with no 'connect' +// listener). +import * as http from "node:http"; +import * as net from "node:net"; + +const server = http.createServer((_req, res) => res.end("request")); +server.on("upgrade", (req: http.IncomingMessage, socket: net.Socket) => { + console.log("upgrade", req.url, req.headers.host); + socket.end("HTTP/1.1 101 Switching Protocols\r\n\r\n"); +}); +function probe(head: string, next: () => void): void { + const port = (server.address() as net.AddressInfo).port; + const raw = net.connect(port, "127.0.0.1", () => raw.write(head)); + let got = ""; + raw.on("data", (d: Buffer) => { + got += d.toString(); + }); + raw.on("close", () => { + console.log("got", JSON.stringify(got.split("\r\n")[0])); + next(); + }); +} +server.listen(0, "127.0.0.1", () => { + probe("GET /ws HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\n\r\n", () => + probe("CONNECT a:1 HTTP/1.1\r\n\r\n", () => + probe("GET / HTTP/1.1\r\nConnection: close\r\n\r\n", () => server.close()), + ), + ); +}); diff --git a/tests/diagnostics/tls-socket-fences.ts b/tests/diagnostics/tls-socket-fences.ts new file mode 100644 index 0000000000..948a8312a1 --- /dev/null +++ b/tests/diagnostics/tls-socket-fences.ts @@ -0,0 +1,20 @@ +// new tls.TLSSocket forms that have no lowering. +import { readFileSync } from "node:fs"; +import * as net from "node:net"; +import * as tls from "node:tls"; + +const cert = readFileSync("cert.pem"); +const key = readFileSync("key.pem"); +const context = tls.createSecureContext({ cert, key }); +function alpn(): string[] { + return ["h2"]; +} + +net.createServer((socket) => { + // The client mode: Node starts no handshake for it. + new tls.TLSSocket(socket, { cert, key }); + // A member after secureContext with side effects. + new tls.TLSSocket(socket, { isServer: true, secureContext: context, ALPNProtocols: alpn() }); + // A secureContext that is not a SecureContext. + new tls.TLSSocket(socket, { isServer: true, secureContext: { cert, key } }); +}); diff --git a/tests/fixtures/node-types/tls-socket-wrap.ts b/tests/fixtures/node-types/tls-socket-wrap.ts new file mode 100644 index 0000000000..4f1272b336 --- /dev/null +++ b/tests/fixtures/node-types/tls-socket-wrap.ts @@ -0,0 +1,31 @@ +// Under @types/node: an http CONNECT handler terminates TLS on the tunnel +// socket with new tls.TLSSocket, and the client runs its handshake inside +// the tunnel with tls.connect({ socket }). +import { readFileSync } from "node:fs"; +import * as http from "node:http"; +import * as net from "node:net"; +import * as tls from "node:tls"; + +const ca = readFileSync("tests/fixtures/server/certs/ca.pem"); +const cert = readFileSync("tests/fixtures/server/certs/localhost.pem"); +const key = readFileSync("tests/fixtures/server/certs/localhost-key.pem"); + +const proxy = http.createServer(); +proxy.on("connect", (req: http.IncomingMessage, socket: net.Socket) => { + socket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + const secure = new tls.TLSSocket(socket, { isServer: true, cert, key, ALPNProtocols: ["http/1.1"] }); + secure.on("secure", () => console.log("proxy secure", req.url, secure.alpnProtocol)); + secure.on("data", (chunk: Buffer) => secure.end(`intercepted ${chunk.toString()}`)); + secure.on("close", () => proxy.close()); +}); +proxy.listen(0, "127.0.0.1", () => { + const port = (proxy.address() as net.AddressInfo).port; + const raw = net.connect(port, "127.0.0.1", () => raw.write("CONNECT example.test:443 HTTP/1.1\r\n\r\n")); + raw.once("data", () => { + const client = tls.connect({ socket: raw, ca, servername: "localhost", ALPNProtocols: ["http/1.1"] }, () => { + console.log("client secureConnect", client.authorized, client.alpnProtocol); + client.write("hello"); + }); + client.on("data", (data: Buffer) => console.log("client data", data.toString())); + }); +}); diff --git a/tests/harness/__snapshots__/tls-socket-fences.ts.txt b/tests/harness/__snapshots__/tls-socket-fences.ts.txt new file mode 100644 index 0000000000..e912b76494 --- /dev/null +++ b/tests/harness/__snapshots__/tls-socket-fences.ts.txt @@ -0,0 +1,26 @@ +tls-socket-fences.ts:15:29 - error SC2020: 'new tls.TLSSocket in client mode' is part of the standard library types but has no scriptc lowering yet + + 14 | // The client mode: Node starts no handshake for it. + 15 | new tls.TLSSocket(socket, { cert, key }); + | ^~~~~~~~~~~~~ + 16 | // A member after secureContext with side effects. + + hint: Node starts no handshake for a client-mode TLSSocket — pass isServer: true for the server side; tls.connect makes client sockets + +tls-socket-fences.ts:17:71 - error SC2020: 'a new tls.TLSSocket options literal with side effects after secureContext' is part of the standard library types but has no scriptc lowering yet + + 16 | // A member after secureContext with side effects. + 17 | new tls.TLSSocket(socket, { isServer: true, secureContext: context, ALPNProtocols: alpn() }); + | ^~~~~~~~~~~~~~~~~~~~~ + 18 | // A secureContext that is not a SecureContext. + + hint: secureContext is read after the other members — move it to the end of the object + +tls-socket-fences.ts:19:62 - error SC2020: 'a '{ cert: Uint8Array; key: Uint8Array }' secureContext' is part of the standard library types but has no scriptc lowering yet + + 18 | // A secureContext that is not a SecureContext. + 19 | new tls.TLSSocket(socket, { isServer: true, secureContext: { cert, key } }); + | ^~~~~~~~~~~~~ + 20 | }); + + hint: pass a tls.createSecureContext({ cert, key }) result \ No newline at end of file diff --git a/tests/harness/corpus-costs/7087-tls-socket-wrap.ts.json b/tests/harness/corpus-costs/7087-tls-socket-wrap.ts.json new file mode 100644 index 0000000000..b4372e38c5 --- /dev/null +++ b/tests/harness/corpus-costs/7087-tls-socket-wrap.ts.json @@ -0,0 +1 @@ +{ "plain": 4491, "san": 82505 } diff --git a/tests/harness/corpus-costs/7088-tls-socket-connect-tunnel.ts.json b/tests/harness/corpus-costs/7088-tls-socket-connect-tunnel.ts.json new file mode 100644 index 0000000000..8e6e4a3d0f --- /dev/null +++ b/tests/harness/corpus-costs/7088-tls-socket-connect-tunnel.ts.json @@ -0,0 +1 @@ +{ "plain": 4100, "san": 56643 } diff --git a/tests/harness/corpus-costs/7089-http-host-header-tunnels.ts.json b/tests/harness/corpus-costs/7089-http-host-header-tunnels.ts.json new file mode 100644 index 0000000000..349a68037f --- /dev/null +++ b/tests/harness/corpus-costs/7089-http-host-header-tunnels.ts.json @@ -0,0 +1 @@ +{ "plain": 3401, "san": 48875 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 00ee400436..96949201dc 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -855,6 +855,56 @@ try { }); }); +// new tls.TLSSocket reads a runtime options record: the client mode, whose +// handshake Node never starts, and members without a lowering throw the +// fence, as do sockets it cannot take over (a second wrap, and +// tls.connect({ socket }) over a socket that is still dialing). +describe("tls.TLSSocket runtime fences", () => { + test("client mode, unlowered members and sockets that cannot be taken over throw", async () => { + const r = await compileAndRun( + "tls-socket-fences", + `import { readFileSync } from "node:fs"; +import * as net from "node:net"; +import * as tls from "node:tls"; +const certs = "tests/fixtures/server/certs"; +const cert = readFileSync(\`\${certs}/localhost.pem\`); +const key = readFileSync(\`\${certs}/localhost-key.pem\`); +function report(label: string, run: () => void): void { + try { + run(); + console.log(label, "no error"); + } catch (error) { + console.log(label, (error as Error).message); + } +} +const client: Record = { cert, key }; +const session: Record = { isServer: true, cert, key, session: Buffer.alloc(1) }; +const server: Record = { isServer: true, cert, key }; +const listener = net.createServer((raw) => { + report("client mode", () => new tls.TLSSocket(raw, client)); + report("session", () => new tls.TLSSocket(raw, session)); + const wrapped = new tls.TLSSocket(raw, server); + report("twice", () => new tls.TLSSocket(raw, server)); + wrapped.destroy(); + listener.close(); +}); +listener.listen(0, () => { + const c = net.connect((listener.address() as net.AddressInfo).port, "localhost"); + report("dialing", () => tls.connect({ socket: c })); + c.on("error", () => {}); +}); +`, + ); + expect(r.stderr).toBe(""); + expect(r.stdout).toBe( + "dialing tls.connect over a socket that is not connected, or is already wrapped, is not supported yet\n" + + "client mode new tls.TLSSocket option 'isServer' has no static lowering — Node starts no handshake for a client-mode TLSSocket; tls.connect({ socket }) runs the client handshake over an open socket\n" + + "session new tls.TLSSocket option 'session' has no static lowering — isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members\n" + + "twice new tls.TLSSocket over a socket that is not connected, or is already wrapped, is not supported yet\n", + ); + }); +}); + // A curve Node lists in crypto.getCurves() but the runtime does not // implement throws a coded SC2020 error naming the curve when the name is // computed; literal names are refused at compile time (node-types-fenced). diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index 8532ad6e90..7a916b4ddc 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -307,6 +307,24 @@ test("node-types: TLSSocket.isSessionReused and keyed ??= timings under @types/n expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: new tls.TLSSocket and tls.connect({ socket }) under @types/node", async () => { + const outDir = outDirFor("tls-socket-wrap"); + const entry = join(nodeTypesDir, "tls-socket-wrap.ts"); + const result = await compile(entry, { + outPath: join(outDir, process.platform === "win32" ? "tls-socket-wrap.exe" : "tls-socket-wrap"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: net.SocketAddress and net.BlockList under @types/node", async () => { const outDir = outDirFor("net-blocklist"); const entry = join(nodeTypesDir, "net-blocklist.ts"); From c09801c06402160567da0632419460a038ae1546 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:08:24 -0500 Subject: [PATCH 104/395] Map a JS binding's declared type only for a nullish write --- packages/compiler/src/frontend/lowering/lower-stmts.ts | 10 ++++++---- packages/compiler/src/frontend/lowering/lowerer.ts | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 2840b0b45e..687420baf3 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -6119,14 +6119,16 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar // types as a primitive (`offset = match[3] || null`): JavaScript files // are checked without strict null checks, so the declared type does not // admit them. - const primitive = lowerer.mapTypeOf(lowerer.typeOf(decl.name)); + // The declared type is mapped only when such a write exists. if ( - (primitive?.kind === "string" || primitive?.kind === "f64" || primitive?.kind === "bool") && candidates.some( (node) => node.operatorToken.kind === ts.SyntaxKind.EqualsToken && mayBeNullish(node.right), ) - ) - return true; + ) { + const primitive = lowerer.mapTypeOf(lowerer.typeOf(decl.name)); + if (primitive?.kind === "string" || primitive?.kind === "f64" || primitive?.kind === "bool") + return true; + } // A plain JavaScript write of a value the declared type cannot hold (the // checker does not report it): buffer-crc32 replaces its number[] table // with `new Int32Array(table)`. Only a different representation counts; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index f3ab173f85..edf0a694b0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3596,7 +3596,7 @@ export class Lowerer { this.genericClassInstanceType(decl, ref, mapArgument), runtimeClassValue: (type) => runtimeClassValueOfType(this, type), localClassValue: (type) => - this.instanceResultClass?.declared === type.id + this.instanceResultClass !== null && this.instanceResultClass.declared === type.id ? this.instanceResultClass.value : (this.localClassValueTypes.get(type.id) ?? null), mixinClassInstance: (decl) => From 1917d1aa003b40808bc5fe2001e8174f2356f62a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:25:02 -0500 Subject: [PATCH 105/395] Test an array read's hole-free prefix before its capacity The dense path of an element read that may be undefined (the `%arr.idxOr` reads behind `a[i]` as `T | undefined`, the store `a[x] = a[y]!`, and the element reads of the lowered array methods) tested the index against the capacity, then against the hole-free prefix (`filled`), and only above the prefix against the length and the slot's state. `filled` is at most the length and the capacity (scr_array.h), so an index below it needs no other test: the read now tests `filled` first and takes the value path on that test alone. Above the prefix it tests the capacity, the length and the state as before. A double index first branches to the runtime lookup when it is not an exact integer, then takes the same tests on its i64 value, which a 32-bit target compares before truncating it. --- .../backend/llvm/local-array-reads.test.ts | 15 +++ .../src/backend/llvm/local-array-reads.ts | 115 +++++++++--------- .../7142-array-read-filled-prefix.ts.json | 6 + tests/corpus/7142-array-read-filled-prefix.ts | 72 +++++++++++ .../7142-array-read-filled-prefix.ts.json | 1 + 5 files changed, 153 insertions(+), 56 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7142-array-read-filled-prefix.ts.json create mode 100644 tests/corpus/7142-array-read-filled-prefix.ts create mode 100644 tests/harness/corpus-costs/7142-array-read-filled-prefix.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 96221942fd..b090298659 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.test.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.test.ts @@ -925,3 +925,18 @@ test.each([ if (consumer !== "local") expect(ir).toContain("local.array.dense"); } }); + +test("a dense read tests the hole-free prefix before the capacity", () => { + const module = immediateFixture(); + for (const bits of [32, 64] as const) { + const ir = workBody(module, bits); + // Field 6 is `filled` and field 2 `cap`: the first branch takes the value + // path below the prefix, and only the range block reads the capacity. + const firstFilled = ir.search(/getelementptr inbounds %ScrArr, ptr %[^,]+, i32 0, i32 6\b/); + const firstCap = ir.search(/getelementptr inbounds %ScrArr, ptr %[^,]+, i32 0, i32 2\b/); + expect(firstFilled).toBeGreaterThan(0); + expect(firstCap).toBeGreaterThan(firstFilled); + expect(ir.slice(firstFilled, firstCap)).toMatch(/br i1 [^,]+, label %local\.array\.value/); + expect(ir.lastIndexOf("local.array.range", firstCap)).toBeGreaterThan(firstFilled); + } +}); diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index b9fd0f54b4..45d046e1f4 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -12,7 +12,7 @@ import { everyStmtList } from "../../ir/traverse.js"; 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, exactIndexLines, loadArrCount, scrArrStateAt } from "./common.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"; @@ -472,78 +472,81 @@ function emitDenseReferenceArrayRead( loadType: "ptr" | "i64" | "i8" = "ptr", ): void { const B = host.B; - const range = B.newLabel("local.array.range"), - dense = B.newLabel("local.array.dense"); - // The dense path uses the existing ScrArr ABI. Sparse indices and - // noncanonical numeric properties retain the runtime lookup semantics. - const capPtr = B.tmp(), - cap = B.tmp(); - B.line( - `${capPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.cap}`, - ); - host.markMemoryPointer(capPtr, "array:header"); - loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr)); - let offset: string, inRange: string; + const header = (field: number): string => { + const ptr = B.tmp(); + B.line(`${ptr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${field}`); + host.markMemoryPointer(ptr, "array:header"); + return ptr; + }; + const valueLabel = B.newLabel("local.array.value"); + // `key` is the index compared with the array's counts and `offset` the + // index that addresses its slots. A double index that is not an exact + // integer, like a sparse or noncanonical one, keeps the runtime lookup + // semantics; an exact one compares as i64, which a 32-bit target does + // before truncating it. + let key: string, keyType: string, offset: string; if (integerIndex) { - offset = integerIndex; - inRange = B.tmp(); - B.line(`${inRange} = icmp ult ${host.sizeType} ${integerIndex}, ${cap}`); + key = offset = integerIndex; + keyType = host.sizeType; } else { - // Capacity bounds the dense slots; the length check below still decides presence. - const exact = exactIndexLines(index.name, cap, host.sizeType, () => B.tmp()); - for (const line of exact.lines) B.line(line); - inRange = exact.ok; - offset = exact.wide; + const raw = B.tmp(), + back = B.tmp(), + integral = B.tmp(), + exact = B.newLabel("local.array.exact"); + key = B.tmp(); + keyType = "i64"; + B.line(`${raw} = fptosi double ${index.name} to i64`); + B.line(`${key} = freeze i64 ${raw}`); + B.line(`${back} = sitofp i64 ${key} to double`); + B.line(`${integral} = fcmp oeq double ${back}, ${index.name}`); + B.condBr(integral, exact, slow); + B.startBlock(exact); + offset = key; if (host.sizeType !== "i64") { offset = B.tmp(); - B.line(`${offset} = trunc i64 ${exact.wide} to ${host.sizeType}`); + B.line(`${offset} = trunc i64 ${key} to ${host.sizeType}`); } } - B.condBr(inRange, range, slow); - B.startBlock(range); - B.br(dense); - B.startBlock(dense); - // The state and value paths below both index from `data`: one load here - // serves both. - const dataPtr = B.tmp(), + const widen = (count: string, type: string): string => { + if (type === keyType) return count; + const wide = B.tmp(); + B.line(`${wide} = zext ${type} ${count} to ${keyType}`); + return wide; + }; + // The state and value paths both index from `data`: one load serves both. + const dataPtr = header(SCR_ARR_FIELD.data), data = B.tmp(); - B.line( - `${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.data}`, - ); - host.markMemoryPointer(dataPtr, "array:header"); B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); - // Below the hole-free prefix bound every state is a present value. - const filledPtr = B.tmp(), + // Below the hole-free prefix every state is a present value, and the + // prefix is at most the length and the capacity, so an index below it needs + // no other test before the value load. + const filledPtr = header(SCR_ARR_FIELD.filled), filled = B.tmp(), - belowFilled = B.tmp(), - lenLabel = B.newLabel("local.array.len"); - B.line( - `${filledPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.filled}`, - ); - host.markMemoryPointer(filledPtr, "array:header"); + prefix = B.tmp(); B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`); - let filledSized = filled; - if (host.sizeType !== "i32") { - filledSized = B.tmp(); - B.line(`${filledSized} = zext i32 ${filled} to ${host.sizeType}`); - } - B.line(`${belowFilled} = icmp ult ${host.sizeType} ${offset}, ${filledSized}`); - const valueLabel = B.newLabel("local.array.value"); + B.line(`${prefix} = icmp ult ${keyType} ${key}, ${widen(filled, "i32")}`); if (induction) { // The state of slot i is at data + cap * (width + 1) - 1 - i, and an // inline state check makes LLVM keep -1 - i as a second induction // variable in the loop; above the prefix the runtime reads the slot. - B.condBr(belowFilled, valueLabel, slow); + B.condBr(prefix, valueLabel, slow); } else { - B.condBr(belowFilled, valueLabel, lenLabel); - B.startBlock(lenLabel); - const lenPtr = B.tmp(), + const range = B.newLabel("local.array.range"), + dense = B.newLabel("local.array.dense"); + B.condBr(prefix, valueLabel, range); + B.startBlock(range); + const capPtr = header(SCR_ARR_FIELD.cap), + cap = B.tmp(), + inRange = B.tmp(); + loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr)); + B.line(`${inRange} = icmp ult ${keyType} ${key}, ${widen(cap, host.sizeType)}`); + B.condBr(inRange, dense, slow); + // Within the capacity and above the prefix, a slot below the length is + // present when its state is. + B.startBlock(dense); + const lenPtr = header(SCR_ARR_FIELD.len), len = B.tmp(), belowLen = B.tmp(); - B.line( - `${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.len}`, - ); - host.markMemoryPointer(lenPtr, "array:header"); loadArrCount(host, len, lenPtr, host.fieldAliasAttachment(lenPtr)); B.line(`${belowLen} = icmp ult ${host.sizeType} ${offset}, ${len}`); const stateLabel = B.newLabel("local.array.state"); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7142-array-read-filled-prefix.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7142-array-read-filled-prefix.ts.json new file mode 100644 index 0000000000..188cd12d44 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7142-array-read-filled-prefix.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7142-array-read-filled-prefix.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7142-array-read-filled-prefix.ts b/tests/corpus/7142-array-read-filled-prefix.ts new file mode 100644 index 0000000000..11cf2f675c --- /dev/null +++ b/tests/corpus/7142-array-read-filled-prefix.ts @@ -0,0 +1,72 @@ +// Element reads that may be undefined (`a[i]` as `T | undefined`, and the +// stores `a[x] = a[y]!` read through) on arrays whose hole-free prefix +// changes: holes from writes past the end and from a longer length, present +// undefined, shrinking and regrowing, shift and unshift, out-of-range and +// negative indices, and reads at a loop counter and at a computed index. +function show(a: (number | undefined)[]): string { + const out: string[] = []; + for (let i = -1; i <= a.length; i++) { + const v = a[i]; + out.push(v === undefined ? (i in a ? "u" : "_") : String(v)); + } + return out.join(","); +} +const a: (number | undefined)[] = [10, 11, 12, 13, 14, 15]; +console.log(show(a)); +a[8] = 18; +console.log(show(a), 7 in a); +a.length = 9; +console.log(show(a)); +a[7] = 17; +console.log(show(a)); +a.length = 3; +console.log(show(a)); +a.push(30, 31); +console.log(show(a)); +a.unshift(1); +console.log(show(a)); +a.shift(); +a.shift(); +console.log(show(a)); +const withUndefined: (number | undefined)[] = [1, undefined, 3]; +console.log(show(withUndefined)); + +// Copies through the read: a hole stores undefined, which `in` then sees. +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 p: number[] = [0, 1, 2, 3, 4]; +rotate(p, 4); +console.log(p.join(",")); +const holes: number[] = new Array(4); +holes[0] = 0; +holes[1] = 1; +holes[3] = 3; +rotate(holes, 3); +console.log(holes.length, 1 in holes, holes[1] === undefined, holes.join(",")); + +// Reads at a computed index and at an index beyond the capacity. +function pick(src: number[], idx: number[]): string { + const out: string[] = []; + for (const k of idx) { + const v = src[k * 2]; + out.push(v === undefined ? "-" : String(v)); + } + return out.join(","); +} +const big: number[] = []; +for (let k = 0; k < 40; k++) big.push(k * k); +console.log(pick(big, [0, 1, 5, 19, 20, 1000, -1, 2 ** 31])); +big.length = 10; +console.log(pick(big, [0, 4, 5, 6])); + +// Swaps at two counters, then reads of the swapped array. +const q: number[] = [5, 4, 3, 2, 1, 0]; +for (let lo = 0, hi = q.length - 1; lo < hi; lo++, hi--) { + const t = q[lo]!; + q[lo] = q[hi]!; + q[hi] = t; +} +console.log(q.join(","), show(q)); diff --git a/tests/harness/corpus-costs/7142-array-read-filled-prefix.ts.json b/tests/harness/corpus-costs/7142-array-read-filled-prefix.ts.json new file mode 100644 index 0000000000..c7b8ddd236 --- /dev/null +++ b/tests/harness/corpus-costs/7142-array-read-filled-prefix.ts.json @@ -0,0 +1 @@ +{ "plain": 3600, "san": 5600 } From 9893630810a26ae8f5ed817108027153a8d74ca3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:26:43 -0500 Subject: [PATCH 106/395] Skip declaration files when looking for writes to a narrowed binding Narrowing stability looked for writes to a binding by walking the whole file that declares it. For globals such as `console` and `JSON` that file is a declaration file (scriptc-node-fallback.d.ts, lib.es5.d.ts), so every build that read one of them materialized about 46,000 AST nodes and kept them until the end of the build. A declaration file holds no code that runs, so none of its writes happen: the walk now returns no writes for it. --- .../compiler/src/frontend/lowering/narrowing-stability.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/compiler/src/frontend/lowering/narrowing-stability.ts b/packages/compiler/src/frontend/lowering/narrowing-stability.ts index 1c991c6ea6..63e5032217 100644 --- a/packages/compiler/src/frontend/lowering/narrowing-stability.ts +++ b/packages/compiler/src/frontend/lowering/narrowing-stability.ts @@ -454,6 +454,10 @@ export class NarrowingStability { const found = new Map(); writes = found; this.identifierWrites.set(file, found); + // A declaration file holds no code that runs, so none of its writes + // happen. Its bindings (`console`, `JSON`) are read in most programs, + // and the walk would read every node of the file. + if (file.isDeclarationFile) return writes; const add = (target: ts.Node): void => { forEachWriteTarget(target, (t) => { if (!ts.isIdentifier(t)) return; From 999fef551e00eb746f3b1a84745ccafb89e0aaf9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:27:07 -0500 Subject: [PATCH 107/395] Resolve repeated checker objects from their ids without decoding them Checker replies name every type, symbol and signature they reach in full, and most of them are objects the client already holds: building tsc-ts, 96% of the types, 79% of the symbols and 80% of the signatures in replies are cached by id. Object replies are now read as bytes, each element's extent is found by a scan that skips strings, and an element whose leading "id" is cached resolves to the cached object without its text being decoded. The other elements are parsed as before. --- .../compiler/src/frontend/ts7/checker.test.ts | 15 +- .../src/frontend/ts7/semantic-checker.ts | 33 ++-- .../src/frontend/ts7/semantic-json.test.ts | 79 ++++++++- .../src/frontend/ts7/semantic-json.ts | 153 ++++++++++++++++++ .../src/frontend/ts7/semantic-model.test.ts | 5 +- .../src/frontend/ts7/semantic-model.ts | 90 ++++++++--- 6 files changed, 326 insertions(+), 49 deletions(-) diff --git a/packages/compiler/src/frontend/ts7/checker.test.ts b/packages/compiler/src/frontend/ts7/checker.test.ts index ac062b2839..cb4a2e5fb9 100644 --- a/packages/compiler/src/frontend/ts7/checker.test.ts +++ b/packages/compiler/src/frontend/ts7/checker.test.ts @@ -283,8 +283,9 @@ test("negative assignability answers remain independent in both directions", () requests.push(query); return JSON.stringify(query.source === 1 && query.target === 2); }, - binary: () => { - throw new Error("unexpected binary request"); + // Object replies are read as bytes; they carry the same JSON. + binary(method, payload) { + return Buffer.from(this.text(method, payload), "utf8"); }, }); const project = snapshot.addProject("project", () => undefined); @@ -324,8 +325,9 @@ test("property lookup caches hits and misses by type and name within each projec checkFlags: 0, }); }, - binary: () => { - throw new Error("unexpected binary request"); + // Object replies are read as bytes; they carry the same JSON. + binary(method, payload) { + return Buffer.from(this.text(method, payload), "utf8"); }, }); const first = snapshot.addProject("first", () => undefined); @@ -397,8 +399,9 @@ test("member type batches are bounded, reuse warm entries, and isolate absent or if (ids.includes(7)) throw new Error("checker panic"); return JSON.stringify(ids.map((id) => (id === 11 ? null : { id, flags: TypeFlags.Number }))); }, - binary: () => { - throw new Error("unexpected binary query"); + // Object replies are read as bytes; they carry the same JSON. + binary(method, payload) { + return Buffer.from(this.text(method, payload), "utf8"); }, }); const project = snapshot.addProject("members", () => undefined); diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.ts b/packages/compiler/src/frontend/ts7/semantic-checker.ts index 92aa77333b..5e7f8339a8 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.ts @@ -1,9 +1,7 @@ import type { SemanticIndexInfoData as IndexInfoResponse, - SemanticSignatureData as SignatureResponse, SemanticSymbolData as SymbolResponse, SemanticTypePredicateData as TypePredicateResponse, - SemanticTypeData as TypeResponse, } from "./semantic-schema.generated.js"; import { AstFile, AstNode } from "./ast-node.js"; import { @@ -14,6 +12,7 @@ import { SemanticType, type SemanticDocument, type SemanticQuery, + present, } from "./semantic-model.js"; import { SemanticSymbolFlags } from "./semantic-schema.generated.js"; import type { TypePredicateKind } from "./enums.generated.js"; @@ -106,32 +105,25 @@ export class SemanticChecker { constructor(readonly project: SemanticProject) {} private type(method: string, query: SemanticQuery): SemanticType | undefined { - const data = this.project.request(method, query); - return data === null ? undefined : this.project.type(data); + return this.project.requestType(method, query); } private symbol(method: string, query: SemanticQuery): SemanticSymbol | undefined { - const data = this.project.request(method, query); - return data === null ? undefined : this.project.symbol(data); + return this.project.requestSymbol(method, query); } private signature(method: string, query: SemanticQuery): SemanticSignature | undefined { - const data = this.project.request(method, query); - return data === null ? undefined : this.project.signature(data); + return this.project.requestSignature(method, query); } private types(method: string, query: SemanticQuery): SemanticType[] { - const data = this.project.request(method, query); - return (data ?? []).map((item) => this.project.type(item)); + return present(this.project.requestTypes(method, query), method); } private symbols(method: string, query: SemanticQuery): SemanticSymbol[] { - const data = this.project.request(method, query); - return (data ?? []).map((item) => this.project.symbol(item)); + return present(this.project.requestSymbols(method, query), method); } private optionalTypes(method: string, query: SemanticQuery): (SemanticType | undefined)[] { - const data = this.project.request<(TypeResponse | null)[]>(method, query); - return data.map((item) => (item === null ? undefined : this.project.type(item))); + return this.project.requestTypes(method, query); } private optionalSymbols(method: string, query: SemanticQuery): (SemanticSymbol | undefined)[] { - const data = this.project.request<(SymbolResponse | null)[]>(method, query); - return data.map((item) => (item === null ? undefined : this.project.symbol(item))); + return this.project.requestSymbols(method, query); } getTypeAtLocation(node: AstNode): SemanticType | undefined; @@ -375,11 +367,10 @@ export class SemanticChecker { ); } getSignaturesOfType(type: SemanticType, kind: number): SemanticSignature[] { - const data = this.project.request("getSignaturesOfType", { - type: type.id, - kind, - }); - return data.map((item) => this.project.signature(item)); + return present( + this.project.requestSignatures("getSignaturesOfType", { type: type.id, kind }), + "getSignaturesOfType", + ); } getTypePredicateOfSignature(signature: SemanticSignature): SemanticTypePredicate | undefined { const data = this.project.request("getTypePredicateOfSignature", { diff --git a/packages/compiler/src/frontend/ts7/semantic-json.test.ts b/packages/compiler/src/frontend/ts7/semantic-json.test.ts index d3b8c4ae39..95bf220f2b 100644 --- a/packages/compiler/src/frontend/ts7/semantic-json.test.ts +++ b/packages/compiler/src/frontend/ts7/semantic-json.test.ts @@ -1,5 +1,10 @@ import { expect, test } from "vitest"; -import { checkSemanticJsonStrings, parseSemanticJson } from "./semantic-json.js"; +import { + checkSemanticJsonStrings, + decodeSemanticObject, + decodeSemanticObjects, + parseSemanticJson, +} from "./semantic-json.js"; test("semantic JSON preserves ordinary strings, escaped slashes, quotes, pairs and nulls", () => { const values = [ @@ -57,3 +62,75 @@ test("malformed JSON remains a parse error rather than an accepted response", () expect(() => parseSemanticJson(json)).toThrow(SyntaxError); } }); + +const replyBytes = (json: string): Uint8Array => Buffer.from(json, "utf8"); + +test("semantic object replies resolve held ids without decoding them and decode the rest", () => { + const held = new Map([[7, { from: "held", id: 7 }]]); + const built: unknown[] = []; + const build = (data: { id: number }) => { + built.push(data); + return { from: "built", id: data.id }; + }; + const reply = JSON.stringify([ + { id: 7, name: "held {not} [read]", declarations: ["1.2./a.ts"] }, + null, + { id: 8, name: 'a "quoted" \\ name ]}', nested: { id: 1, list: [1, { x: "}" }] } }, + { name: "id last", id: 9 }, + ]); + expect(decodeSemanticObjects(replyBytes(reply), (id) => held.get(id), build)).toEqual([ + { from: "held", id: 7 }, + null, + { from: "built", id: 8 }, + { from: "built", id: 9 }, + ]); + expect(built).toEqual([JSON.parse(reply)[2], JSON.parse(reply)[3]]); + // The text of a held object is not decoded. + expect( + decodeSemanticObjects( + replyBytes('[{"id":7,"bad":\\ud800}, {"id":7}]'), + (id) => held.get(id), + build, + ), + ).toEqual([ + { from: "held", id: 7 }, + { from: "held", id: 7 }, + ]); +}); + +test("semantic object replies accept null, empty and single replies", () => { + const build = (data: { id: number }) => data.id; + const none = (): number | undefined => undefined; + expect(decodeSemanticObjects(replyBytes(""), none, build)).toBeNull(); + expect(decodeSemanticObjects(replyBytes("null"), none, build)).toBeNull(); + expect(decodeSemanticObjects(replyBytes(" [ ] "), none, build)).toEqual([]); + expect(decodeSemanticObjects(replyBytes('[{"id":3},\n{"id":4}]'), none, build)).toEqual([3, 4]); + expect(decodeSemanticObject(replyBytes(""), none, build)).toBeNull(); + expect(decodeSemanticObject(replyBytes("null"), none, build)).toBeNull(); + expect(decodeSemanticObject(replyBytes('{"id":12,"name":"x"}'), none, build)).toBe(12); + expect(decodeSemanticObject(replyBytes('{"id":12}'), (id) => id * 2, build)).toBe(24); + // Non-ASCII text in a decoded object round-trips. + expect( + decodeSemanticObject( + replyBytes(JSON.stringify({ id: 5, name: "héllo 🌍" })), + (): string | undefined => undefined, + (data: { id: number; name: string }) => data.name, + ), + ).toBe("héllo 🌍"); +}); + +test("semantic object replies reject malformed JSON", () => { + const build = (data: { id: number }) => data.id; + const none = (): number | undefined => undefined; + for (const json of [ + '[{"id":1}', + '[{"id":1},]', + '{"id":1} {"id":2}', + '[{"id":1} {"id":2}]', + '"open', + "nul", + ]) + expect(() => decodeSemanticObjects(replyBytes(json), none, build)).toThrow(); + for (const json of ['{"id":1', '{"id":1}}', "nul"]) + expect(() => decodeSemanticObject(replyBytes(json), none, build)).toThrow(); +}); diff --git a/packages/compiler/src/frontend/ts7/semantic-json.ts b/packages/compiler/src/frontend/ts7/semantic-json.ts index 32456c95a5..3a04c741f8 100644 --- a/packages/compiler/src/frontend/ts7/semantic-json.ts +++ b/packages/compiler/src/frontend/ts7/semantic-json.ts @@ -49,3 +49,156 @@ export function parseSemanticJson(json: string): T { checkSemanticJsonStrings(json, preservesJsonSurrogates); return JSON.parse(json) as T; } + +const OPEN_BRACE = 0x7b; +const CLOSE_BRACE = 0x7d; +const OPEN_BRACKET = 0x5b; +const CLOSE_BRACKET = 0x5d; +const QUOTE = 0x22; +const BACKSLASH = 0x5c; +const COMMA = 0x2c; +const LOWER_N = 0x6e; +/** `{"id":`, the start of every object reply the checker sends. */ +const ID_PREFIX = [OPEN_BRACE, QUOTE, 0x69, 0x64, QUOTE, 0x3a]; + +function isJsonSpace(byte: number): boolean { + return byte === 0x20 || byte === 0x0a || byte === 0x0d || byte === 0x09; +} + +function skipJsonSpace(bytes: Uint8Array, index: number): number { + while (index < bytes.length && isJsonSpace(bytes[index]!)) index++; + return index; +} + +function malformedReply(): never { + throw new Error("TypeScript semantic response: malformed JSON"); +} + +/** The index after the JSON value that starts at `start`: an object, an + * array, a string or a bare literal. Strings are skipped with their escapes, + * so brackets inside them do not count. */ +function jsonValueEnd(bytes: Uint8Array, start: number): number { + let depth = 0; + let index = start; + while (index < bytes.length) { + const byte = bytes[index]!; + if (byte === QUOTE) { + index++; + while (index < bytes.length && bytes[index] !== QUOTE) + index += bytes[index] === BACKSLASH ? 2 : 1; + if (index >= bytes.length) malformedReply(); + index++; + if (depth === 0) return index; + continue; + } + if (byte === OPEN_BRACE || byte === OPEN_BRACKET) depth++; + else if (byte === CLOSE_BRACE || byte === CLOSE_BRACKET) { + if (depth === 0) return index; + depth--; + if (depth === 0) return index + 1; + } else if (depth === 0 && (byte === COMMA || isJsonSpace(byte))) return index; + index++; + } + if (depth !== 0) malformedReply(); + return index; +} + +/** The id an object reply starts with (`{"id":N`), or -1 when it starts + * otherwise. */ +function leadingId(bytes: Uint8Array, start: number, end: number): number { + if (end - start < ID_PREFIX.length + 1) return -1; + for (let k = 0; k < ID_PREFIX.length; k++) if (bytes[start + k] !== ID_PREFIX[k]) return -1; + let index = start + ID_PREFIX.length; + let id = 0; + let digits = 0; + while (index < end && bytes[index]! >= 0x30 && bytes[index]! <= 0x39) { + id = id * 10 + (bytes[index]! - 0x30); + index++; + digits++; + } + if (digits === 0 || digits > 15 || (bytes[index] !== COMMA && bytes[index] !== CLOSE_BRACE)) + return -1; + return id; +} + +function parseJsonSlice(bytes: Uint8Array, start: number, end: number): T { + const text = Buffer.from(bytes.buffer, bytes.byteOffset + start, end - start).toString("utf8"); + return parseSemanticJson(text); +} + +/** One element of a reply: null, or the object with leading id `id`. Objects + * the caller already holds under that id are not decoded. */ +function replyElement( + bytes: Uint8Array, + start: number, + end: number, + cached: (id: number) => R | undefined, + build: (data: T) => R, +): R | null { + if (bytes[start] === LOWER_N) { + if (parseJsonSlice(bytes, start, end) !== null) malformedReply(); + return null; + } + const id = leadingId(bytes, start, end); + if (id >= 0) { + const known = cached(id); + if (known !== undefined) return known; + } + return build(parseJsonSlice(bytes, start, end)); +} + +/** A checker reply that is one object (with a numeric `id`) or null. An empty + * reply is null. Objects the caller already holds under their id, which a + * reply repeats whenever it names them, are returned from `cached` without + * decoding their text; the rest are decoded and handed to `build`. */ +export function decodeSemanticObject( + bytes: Uint8Array, + cached: (id: number) => R | undefined, + build: (data: T) => R, +): R | null { + const start = skipJsonSpace(bytes, 0); + if (start === bytes.length) return null; + const end = jsonValueEnd(bytes, start); + if (skipJsonSpace(bytes, end) !== bytes.length) malformedReply(); + return replyElement(bytes, start, end, cached, build); +} + +/** A checker reply that is an array of objects (each with a numeric `id`) and + * nulls, or null. An empty reply is null. Elements are resolved as in + * decodeSemanticObject. */ +export function decodeSemanticObjects( + bytes: Uint8Array, + cached: (id: number) => R | undefined, + build: (data: T) => R, +): (R | null)[] | null { + let index = skipJsonSpace(bytes, 0); + if (index === bytes.length) return null; + if (bytes[index] !== OPEN_BRACKET) { + const end = jsonValueEnd(bytes, index); + if ( + skipJsonSpace(bytes, end) !== bytes.length || + parseJsonSlice(bytes, index, end) !== null + ) + malformedReply(); + return null; + } + const result: (R | null)[] = []; + index = skipJsonSpace(bytes, index + 1); + if (bytes[index] === CLOSE_BRACKET) { + if (skipJsonSpace(bytes, index + 1) !== bytes.length) malformedReply(); + return result; + } + for (;;) { + const end = jsonValueEnd(bytes, index); + if (end === index) malformedReply(); + result.push(replyElement(bytes, index, end, cached, build)); + index = skipJsonSpace(bytes, end); + if (bytes[index] === COMMA) { + index = skipJsonSpace(bytes, index + 1); + continue; + } + if (bytes[index] !== CLOSE_BRACKET || skipJsonSpace(bytes, index + 1) !== bytes.length) + malformedReply(); + return result; + } +} diff --git a/packages/compiler/src/frontend/ts7/semantic-model.test.ts b/packages/compiler/src/frontend/ts7/semantic-model.test.ts index 36a445bc81..9d0f61a4ea 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.test.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.test.ts @@ -45,8 +45,9 @@ function harness() { if (!answers.has(method)) throw new Error(`Unexpected request ${method}`); return JSON.stringify(answers.get(method)); }, - binary() { - throw new Error("Unexpected binary request"); + // Object replies are read as bytes; they carry the same JSON. + binary(method, payload) { + return Buffer.from(this.text(method, payload), "utf8"); }, }); const first = snapshot.addProject("/first.json", () => undefined); diff --git a/packages/compiler/src/frontend/ts7/semantic-model.ts b/packages/compiler/src/frontend/ts7/semantic-model.ts index 125e324c7a..d3d5fe61a3 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -8,7 +8,7 @@ import type { __String } from "./ast-types.js"; import type * as enums from "./enums.generated.js"; import { parseAstNodeHandle } from "./ast-wire.js"; import type { SemanticChecker, SemanticJsDocTag } from "./semantic-checker.js"; -import { parseSemanticJson } from "./semantic-json.js"; +import { decodeSemanticObject, decodeSemanticObjects, parseSemanticJson } from "./semantic-json.js"; import { SemanticObjectFlags as ObjectFlags, SemanticSignatureFlags as SignatureFlags, @@ -17,6 +17,14 @@ import { /** JSON and binary operations stay at the transport boundary. The concrete * semantic objects below need neither the SDK nor a JavaScript engine. */ +/** The elements of a reply that has no nulls: a null element fails as it + * would when read as an object. */ +export function present(items: (T | undefined)[], method: string): T[] { + for (const item of items) + if (item === undefined) throw new TypeError(`${method} returned a null element`); + return items as T[]; +} + export interface SemanticTransport { text: (method: string, payload: string) => string; binary: (method: string, payload: string) => Uint8Array; @@ -154,6 +162,54 @@ export class SemanticProject { return this.snapshot.transport.binary(method, JSON.stringify(query)); } + /** A reply of one type, or undefined for null. */ + requestType(method: string, query: SemanticQuery): SemanticType | undefined { + const bytes = this.requestBinary(method, query); + const cached = (id: number): SemanticType | undefined => this.types.get(id); + const build = (data: TypeResponse): SemanticType => this.type(data); + return decodeSemanticObject(bytes, cached, build) ?? undefined; + } + + /** A reply of types and nulls (undefined here), or [] for null. */ + requestTypes(method: string, query: SemanticQuery): (SemanticType | undefined)[] { + const bytes = this.requestBinary(method, query); + const cached = (id: number): SemanticType | undefined => this.types.get(id); + const build = (data: TypeResponse): SemanticType => this.type(data); + return (decodeSemanticObjects(bytes, cached, build) ?? []).map((item) => item ?? undefined); + } + + /** A reply of one symbol, or undefined for null. */ + requestSymbol(method: string, query: SemanticQuery): SemanticSymbol | undefined { + const bytes = this.requestBinary(method, query); + const cached = (id: number): SemanticSymbol | undefined => this.snapshot.cachedSymbol(id); + const build = (data: SymbolResponse): SemanticSymbol => this.symbol(data); + return decodeSemanticObject(bytes, cached, build) ?? undefined; + } + + /** A reply of symbols and nulls (undefined here), or [] for null. */ + requestSymbols(method: string, query: SemanticQuery): (SemanticSymbol | undefined)[] { + const bytes = this.requestBinary(method, query); + const cached = (id: number): SemanticSymbol | undefined => this.snapshot.cachedSymbol(id); + const build = (data: SymbolResponse): SemanticSymbol => this.symbol(data); + return (decodeSemanticObjects(bytes, cached, build) ?? []).map((item) => item ?? undefined); + } + + /** A reply of one signature, or undefined for null. */ + requestSignature(method: string, query: SemanticQuery): SemanticSignature | undefined { + const bytes = this.requestBinary(method, query); + const cached = (id: number): SemanticSignature | undefined => this.signatures.get(id); + const build = (data: SignatureResponse): SemanticSignature => this.signature(data); + return decodeSemanticObject(bytes, cached, build) ?? undefined; + } + + /** A reply of signatures and nulls (undefined here), or [] for null. */ + requestSignatures(method: string, query: SemanticQuery): (SemanticSignature | undefined)[] { + const bytes = this.requestBinary(method, query); + const cached = (id: number): SemanticSignature | undefined => this.signatures.get(id); + const build = (data: SignatureResponse): SemanticSignature => this.signature(data); + return (decodeSemanticObjects(bytes, cached, build) ?? []).map((item) => item ?? undefined); + } + type(data: TypeResponse): SemanticType { this.ensureActive(); let type = this.types.get(data.id); @@ -190,9 +246,9 @@ export class SemanticProject { const cached = this.types.get(handle); if (cached !== undefined) return cached; } - const data = this.request(method, { objectId: source }); - if (data === null) throw new Error(`${method} returned null type for ${source}`); - return this.type(data); + const type = this.requestType(method, { objectId: source }); + if (type === undefined) throw new Error(`${method} returned null type for ${source}`); + return type; } fetchSymbol( @@ -204,9 +260,9 @@ export class SemanticProject { if (handle === undefined || handle === 0) return undefined; const cached = this.snapshot.cachedSymbol(handle); if (cached !== undefined) return cached; - const data = this.request(method, { objectId: source }); - if (data === null) throw new Error(`${method} returned null symbol for ${source}`); - return this.symbol(data); + const symbol = this.requestSymbol(method, { objectId: source }); + if (symbol === undefined) throw new Error(`${method} returned null symbol for ${source}`); + return symbol; } fetchSignature( @@ -218,9 +274,9 @@ export class SemanticProject { if (handle === undefined || handle === 0) return undefined; const cached = this.signatures.get(handle); if (cached !== undefined) return cached; - const data = this.request(method, { objectId: source }); - if (data === null) throw new Error(`${method} returned null signature for ${source}`); - return this.signature(data); + const signature = this.requestSignature(method, { objectId: source }); + if (signature === undefined) throw new Error(`${method} returned null signature for ${source}`); + return signature; } fetchTypes(source: number, method: string, handles?: readonly number[]): SemanticType[] { @@ -234,8 +290,7 @@ export class SemanticProject { } if (cached.length === handles.length) return cached; } - const data = this.request(method, { objectId: source }); - return (data ?? []).map((item) => this.type(item)); + return present(this.requestTypes(method, { objectId: source }), method); } fetchSymbols(source: number, method: string, handles?: readonly number[]): SemanticSymbol[] { @@ -249,8 +304,7 @@ export class SemanticProject { } if (cached.length === handles.length) return cached; } - const data = this.request(method, { objectId: source }); - return (data ?? []).map((item) => this.symbol(item)); + return present(this.requestSymbols(method, { objectId: source }), method); } dispose(): void { @@ -577,9 +631,8 @@ export class SemanticType { * changes nothing or its members are unreadable too. */ private widenedConstituents(): SemanticType[] | undefined { try { - const data = this.project.request("getWidenedType", { type: this.id }); - if (data === null || data.id === this.id) return undefined; - const widened = this.project.type(data); + const widened = this.project.requestType("getWidenedType", { type: this.id }); + if (widened === undefined || widened.id === this.id) return undefined; if ((widened.flags & TypeFlags.Union) === 0) return [widened]; return this.project.fetchTypes(widened.id, "getTypesOfType"); } catch (e) { @@ -594,8 +647,7 @@ export class SemanticType { getBaseTypes(): SemanticType[] | undefined { this.project.ensureActive(); if (!this.isClassOrInterface()) return undefined; - const data = this.project.request("getBaseTypes", { type: this.id }); - return (data ?? []).map((item) => this.project.type(item)); + return present(this.project.requestTypes("getBaseTypes", { type: this.id }), "getBaseTypes"); } getTrueType(): SemanticType { const result = this.project.fetchType(this.id, "getTrueTypeOfConditionalType", this.trueType); From bde0b64004a692c8254138475f7168439db3b3c4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:36:03 -0500 Subject: [PATCH 108/395] 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 778ef152bc..08d89e8505 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 6bf3b7d9df9b0a9c506392570f2fb8bece333ffb Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:44:28 -0500 Subject: [PATCH 109/395] Install driver-built translation units before the native build The private copy of the LLVM text had no extension. Builds that compile it with the C driver (sanitize builds, targets without the LLVM helper) passed it to clang directly when the native cache was off, and clang read it as a linker input, so the build failed. The copy is now named with `.ll`. Those builds also record the output directory in their native cache proof. Renaming the private copies to the shared names after the native build changed that directory, so every unchanged repeat missed the proof and relinked the executable. The shared names are now installed before the native build, which compiles and caches copies in a private directory outside the output directory. --- packages/cli/test/executable-cache.test.ts | 56 +++++++++++++- .../src/executable/executable-cache.test.ts | 2 +- .../src/executable/executable-cache.ts | 3 +- packages/compiler/src/index.ts | 77 +++++++++++++++++-- 4 files changed, 127 insertions(+), 11 deletions(-) diff --git a/packages/cli/test/executable-cache.test.ts b/packages/cli/test/executable-cache.test.ts index a0131f14fe..ee80277653 100644 --- a/packages/cli/test/executable-cache.test.ts +++ b/packages/cli/test/executable-cache.test.ts @@ -1,12 +1,13 @@ import { execFile } from "node:child_process"; import { createRequire } from "node:module"; -import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { chmod, mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { delimiter, dirname, join } from "node:path"; import { promisify } from "node:util"; import { expect, test } from "vitest"; import { precompiledRuntimePackTarget } from "../../compiler/src/startup-cache.js"; import { platformLinkerSupportsPersistentCache } from "../../compiler/src/backend/linker.js"; +import { toolchainEnvironmentCachePolicy } from "../../compiler/src/backend/toolchain-environment.js"; const execFileAsync = promisify(execFile); const require = createRequire(import.meta.url); @@ -102,6 +103,59 @@ test("exact executable repeats skip lowering while edits and damaged outputs sta } }, 120_000); +// Sanitize builds compile the LLVM text with the C driver rather than linking a precompiled +// runtime pack, and that route's native cache proof records the output directory. +test.skipIf(process.platform === "win32")( + "sanitize builds compile uncached and keep the executable on unchanged repeats", + async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-cli-executable-sanitize-")); + const cacheRoot = join(dir, "cache"); + const entry = join(dir, "main.ts"); + const outPath = join(dir, "program"); + const build = async (env: NodeJS.ProcessEnv = {}): Promise<{ stderr: string }> => { + const { stderr } = await execFileAsync( + process.execPath, + ["--import", tsxLoader, cliEntry, "build", entry, "--sanitize", "-o", outPath], + { + env: { ...process.env, SCRIPTC_CACHE_DIR: cacheRoot, SCRIPTC_TIMING: "1", ...env }, + maxBuffer: 4 * 1024 * 1024, + }, + ); + return { stderr }; + }; + const run = async (): Promise => + (await execFileAsync(outPath, [], { maxBuffer: 1024 * 1024 })).stdout; + try { + await mkdir(cacheRoot, { mode: 0o700 }); + await writeFile(entry, 'console.log("sanitized");\n'); + + // With the native cache off, clang compiles the build's private copy of the LLVM text. + await build({ SCRIPTC_NO_CACHE: "1" }); + expect(await run()).toBe("sanitized\n"); + + expect((await build()).stderr).toContain("scriptc lowering"); + expect(await run()).toBe("sanitized\n"); + const built = await stat(outPath); + const tuTime = (await stat(join(dir, "main.ll"))).mtimeMs; + + // The output directory does not change after the native cache proof is recorded, so an + // unchanged repeat keeps the executable instead of linking it again. + expect((await build()).stderr).not.toContain("scriptc lowering"); + if (toolchainEnvironmentCachePolicy(process.env).completeArtifacts) { + const repeated = await stat(outPath); + expect(repeated.ino).toBe(built.ino); + expect(repeated.mtimeMs).toBe(built.mtimeMs); + } + expect(await run()).toBe("sanitized\n"); + expect((await stat(join(dir, "main.ll"))).mtimeMs).toBe(tuTime); + expect((await readdir(dir)).sort()).toEqual(["cache", "main.ll", "main.ts", "program"]); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, + 240_000, +); + test.skipIf(process.platform === "win32")( "an executable appearing earlier on unchanged PATH invalidates the early binary", async () => { diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index e7c57d1b01..2de6885310 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -137,7 +137,7 @@ test("early executable cache restores the emitted TU, IR, and native feature gat // them there when it is done. expect(hit?.llvmPath).toBe(f.llvmPath); expect(hit?.irPath).toBe(f.irPath); - expect(hit?.llvmSourcePath).toMatch(/[/\\]\.scriptc-llvm-/); + expect(hit?.llvmSourcePath).toMatch(/[/\\]\.scriptc-llvm-[^/\\]*\.ll$/); expect(hit?.irSourcePath).toMatch(/[/\\]\.scriptc-ir-/); expect(await readFile(hit!.llvmSourcePath!, "utf8")).toBe("; generated llvm\n"); expect(await readFile(hit!.irSourcePath!, "utf8")).toContain("irVersion"); diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 941a7acdcf..be94efb0b9 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -615,7 +615,8 @@ async function readExecutableCache( ) await installBytes(ir, paths.irPath); } else { - llvmSourcePath = privateSiblingPath(paths.llvmPath, "llvm"); + // The extension keeps clang reading the copy as LLVM IR rather than as a linker input. + llvmSourcePath = `${privateSiblingPath(paths.llvmPath, "llvm")}.ll`; await installBytes(translationUnit, llvmSourcePath); llvmPathCurrent = await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest); if (ir !== null && stamp.files.ir !== null) { diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index db1b0b3e34..7d6ab9b6df 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -49,7 +49,8 @@ import { frontendProcessExecArgv } from "./frontend-process-args.js"; import { type ChildProcess, spawn } from "node:child_process"; import { createHash } from "node:crypto"; import { existsSync } from "node:fs"; -import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"; +import { copyFile, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { @@ -850,7 +851,8 @@ async function prepareExecutableInput( await mkdir(opts.outDir, { recursive: true }); const llvmPath = defaultSourcePaths.llvm; - const llvmSourcePath = privateSiblingPath(llvmPath, "llvm"); + // The extension keeps clang reading the copy as LLVM IR rather than as a linker input. + const llvmSourcePath = `${privateSiblingPath(llvmPath, "llvm")}.ll`; const backend = "llvm" as const; const useRuntimePack = opts.nativeProgramObject === true || usesPrecompiledRuntimePack(opts, backend); @@ -912,6 +914,8 @@ async function prepareExecutableInput( * build therefore writes, compiles and caches private copies (privateSiblingPath) and moves them to * the shared names only when it is done (rename replaces a file in one step): a concurrent build of * another program cannot replace the text a build compiles or the translation unit it caches. + * Builds that compile with the C driver install them before the native build instead + * (stageDriverBuildSources). */ interface BuildSources { llvmPath: string; @@ -941,6 +945,40 @@ async function installBuildSources(sources: BuildSources): Promise { ]); } +/** + * A build that compiles the LLVM text with the C driver (no precompiled runtime pack) records the + * output directory in its native cache proofs, so that directory has to be in its final state before + * the native build starts. The build therefore installs the shared names first and compiles and + * caches copies in a private directory outside the output directory. Returns that directory and the + * copies. + */ +async function stageDriverBuildSources( + sources: BuildSources, +): Promise<{ dir: string; sources: BuildSources }> { + const dir = await mkdtemp(join(tmpdir(), "scriptc-program-")); + const llvmSourcePath = join(dir, basename(sources.llvmPath)); + const irSourcePath = + sources.irPath === undefined || sources.irSourcePath === undefined + ? undefined + : join(dir, basename(sources.irPath)); + try { + await Promise.all([ + copyFile(sources.llvmSourcePath, llvmSourcePath), + irSourcePath === undefined || sources.irSourcePath === undefined + ? undefined + : copyFile(sources.irSourcePath, irSourcePath), + ]); + } catch (err) { + await rm(dir, { recursive: true, force: true }); + throw err; + } + await installBuildSources(sources); + return { + dir, + sources: { llvmPath: sources.llvmPath, llvmSourcePath, irPath: sources.irPath, irSourcePath }, + }; +} + /** * The child process that prepares an executable build's input: frontend, lowering and LLVM emission * (`runFrontendProcess`). Node's heap keeps the pages a build's frontend used for the life of the @@ -1515,6 +1553,9 @@ async function compileTracked( llvmPathCurrent: earlyHit.llvmPathCurrent, irPathCurrent: earlyHit.irPathCurrent, }; + // What the native build compiles and the cache publishes: hitSources, or their staged copies. + let nativeSources = hitSources; + let stagedDir: string | null = null; let nativeInputPath = hitSources.llvmSourcePath; let nativeProgramObject: NativeProgramObject | null = null; const useRuntimePack = @@ -1545,6 +1586,11 @@ async function compileTracked( sourceTexts: new Map(), }; } + } else { + const staged = await stageDriverBuildSources(hitSources); + stagedDir = staged.dir; + nativeSources = staged.sources; + nativeInputPath = nativeSources.llvmSourcePath; } await compileExecutableNative( earlyHit.native, @@ -1561,10 +1607,10 @@ async function compileTracked( : async ({ dependencies }) => { await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, { ...earlyHit, - llvmPath: hitSources.llvmSourcePath, - ...(hitSources.irSourcePath === undefined + llvmPath: nativeSources.llvmSourcePath, + ...(nativeSources.irSourcePath === undefined ? {} - : { irPath: hitSources.irSourcePath }), + : { irPath: nativeSources.irSourcePath }), executableRestored: true, nativeDependencies: dependencies, frontend: earlyHit.frontend, @@ -1602,7 +1648,8 @@ async function compileTracked( nativeProgramObject.partitionPaths, ); } - await installBuildSources(hitSources); + if (stagedDir === null) await installBuildSources(hitSources); + else await rm(stagedDir, { recursive: true, force: true }); } await pruneBuildCache(cacheRoot); return { @@ -1635,8 +1682,17 @@ async function compileTracked( ) : await prepareExecutableInput(entryPath, opts, ffi, buildPlatform, timing, warningSink); if ("ok" in prepared) return prepared; - const { llvmSource, llvmPath, llvmSourcePath, irPath, irSourcePath } = prepared; + const buildSources: BuildSources = { + llvmPath: prepared.llvmPath, + llvmSourcePath: prepared.llvmSourcePath, + irPath: prepared.irPath, + irSourcePath: prepared.irSourcePath, + }; + const { llvmSource, llvmPath, irPath } = prepared; const { nativeFeatures, programSplit, sourceTexts } = prepared; + // What the native build compiles and the cache publishes: buildSources, or their staged copies. + let { llvmSourcePath, irSourcePath } = buildSources; + let stagedDir: string | null = null; const backend = "llvm" as const; await mkdir(dirname(opts.outPath), { recursive: true }); if (earlyCacheOptions === null) { @@ -1668,6 +1724,10 @@ async function compileTracked( sourceTexts, }; } + } else { + const staged = await stageDriverBuildSources(buildSources); + stagedDir = staged.dir; + ({ llvmSourcePath, irSourcePath } = staged.sources); } await compileExecutableNative( nativeFeatures, @@ -1732,7 +1792,8 @@ async function compileTracked( nativeProgramObject.partitionPaths, ); } - await installBuildSources({ llvmPath, llvmSourcePath, irPath, irSourcePath }); + if (stagedDir === null) await installBuildSources(buildSources); + else await rm(stagedDir, { recursive: true, force: true }); } await pruneBuildCache(cacheRoot); timing("complete"); From aeb429c066fec555512e23abb04d5cfd962863bc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:45:38 -0500 Subject: [PATCH 110/395] 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 ae36f736d1..954b040db4 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 85a232bb2a..409b9cb302 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 c08d7b720f..4533922432 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 37f77f4c69..79017c97aa 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 17cb2efc09..625ee93afe 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 59c285eadb..b4de8db774 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 30f504b4a5..3980294747 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 8f119504cb..cb2d868048 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 61558f0080..759687ff0e 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 653c0d837e..ea7d7957bf 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 5b78c7278d..5238333ad8 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 edf0a694b0..7838213b2c 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 9abd5a0b0c..93349054a4 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 8f5928d12d..70ec7d5026 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 0000000000..d22715215e --- /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 0000000000..313888b490 --- /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 a427fc9a1a..95f251bd43 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 4e21fa397c..906be0ccd7 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 0000000000..ec08a297a1 --- /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 0000000000..c32a820c29 --- /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 0000000000..adeea728e1 --- /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 0000000000..20eee362fd --- /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 0000000000..a469a45f31 --- /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 0000000000..7f8db4eb85 --- /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 0000000000..22aad4e0a7 --- /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 53eac6c3c3..6291c291f3 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 0000000000..9495bb65ca --- /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 0000000000..f84e214375 --- /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 86d202b461..40b55b0784 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 6f11afa09e85010c6b15c3e515b93d160d3379a5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:46:53 -0500 Subject: [PATCH 111/395] Size crypto error messages to their whole text The key and signature errors formatted into fixed stack buffers and passed snprintf's return value on as the message length. A message longer than the buffer, such as "Invalid digest: " with a long algorithm name from crypto.sign or crypto.verify, then copied bytes past the end of the buffer into the error. The messages are now sized to their whole text, so a long name appears in full, as in Node. The received values in these messages also follow Node: strings as util.inspect quotes them, cut at 128 characters, and large integers with ERR_OUT_OF_RANGE's separators. A fractional, NaN or infinite modulusLength reports "It must be an integer", as validateUint32 does. --- packages/runtime/src/scr_pki.c | 98 +++++++++++++++--------- packages/runtime/src/scr_pki.h | 4 + packages/runtime/src/scr_webcrypto_pki.c | 10 +-- tests/corpus/7083-crypto-sign-verify.ts | 3 + tests/corpus/7084-crypto-key-pairs.cjs | 14 ++++ 5 files changed, 86 insertions(+), 43 deletions(-) diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c index 1b102fd22c..54aadc06b3 100644 --- a/packages/runtime/src/scr_pki.c +++ b/packages/runtime/src/scr_pki.c @@ -12,6 +12,8 @@ * All arguments are borrowed. */ #include "scr_runtime.h" +#include +#include #include #include #include @@ -37,14 +39,41 @@ static void sp_throw(int kind, const char *code, const char *message) { scr_throw_error_msg_code(kind, message, strlen(message), code); } +void scr_pki_throw_fmt(int kind, const char *code, const char *fmt, ...) { + va_list ap, sized; + va_start(ap, fmt); + va_copy(sized, ap); + int n = vsnprintf(NULL, 0, fmt, sized); + va_end(sized); + if (n < 0) n = 0; + char *message = malloc((size_t)n + 1); + if (!message) scr_trap("scriptc: out of memory\n"); + vsnprintf(message, (size_t)n + 1, fmt, ap); + va_end(ap); + if (code) scr_throw_error_msg_code(kind, message, (size_t)n, code); + else scr_throw_error_msg(kind, message, (size_t)n); + free(message); +} + +/* The "Received" text of Node's ERR_INVALID_ARG_VALUE and ERR_OUT_OF_RANGE + * messages: a string as util.inspect quotes it (cut at 128 UTF-16 units + * plus "..."), a number as ERR_OUT_OF_RANGE prints it when `range`, any + * other value as its sketch. +1. */ +static ScrStr *sp_received(const ScrDyn *got, bool range) { + if (got->kind == SCR_DYN_STR) return scr_str_inspect_quoted(got->v.str, true); + char text[96]; + if (range && got->kind == SCR_DYN_NUM) return scr_str_new(text, scr_num_received(got->v.num, text)); + const char *lite = scr_dyn_inspect_lite(got, text, sizeof text); + return scr_str_new(lite, strlen(lite)); +} + /* ERR_INVALID_ARG_VALUE for an option ("The property 'options.type' is * invalid. Received 'spki'"). */ static void sp_property_invalid(const char *name, const ScrDyn *got) { - char lite[96]; - char message[256]; - int n = snprintf(message, sizeof message, "The property '%s' is invalid. Received %s", name, - scr_dyn_inspect_lite(got, lite, sizeof lite)); - scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)n, "ERR_INVALID_ARG_VALUE"); + ScrStr *received = sp_received(got, false); + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "The property '%s' is invalid. Received %.*s", name, + (int)received->len, received->data); + scr_str_release(received); } static void sp_unsupported_input(void) { @@ -53,10 +82,8 @@ static void sp_unsupported_input(void) { /* The coded refusal for a form this runtime does not implement. */ static void sp_not_supported(const char *what) { - char message[256]; - int n = snprintf(message, sizeof message, - "'%s' is part of the standard library types but has no scriptc lowering yet", what); - scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020"); + scr_pki_throw_fmt(SCR_ERR_ERROR, "SC2020", "'%s' is part of the standard library types but has no scriptc lowering yet", + what); } static int sp_random(void *state, unsigned char *out, size_t len) { @@ -1066,9 +1093,7 @@ static ScrDyn *sp_key_invoke(void *h, ScrDyn *self, const char *method, ScrDyn * sp_not_supported("KeyObject.toCryptoKey"); return NULL; } - char message[160]; - int n = snprintf(message, sizeof message, "%s is not a function", what); - scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "%s is not a function", what); return NULL; } @@ -1093,10 +1118,8 @@ static bool sp_key_set(void *h, const char *name, size_t len, const ScrDyn *valu ? "AsymmetricKeyObject" : NULL; if (!holder) return false; - char message[160]; - int n = snprintf(message, sizeof message, "Cannot set property %.*s of #<%s> which has only a getter", - (int)len, name, holder); - scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "Cannot set property %.*s of #<%s> which has only a getter", (int)len, name, + holder); return true; } @@ -1210,12 +1233,11 @@ static SpKey *sp_generate(const ScrDyn *type, const ScrDyn *options) { bool other = false; for (size_t i = 0; i < sizeof known / sizeof known[0]; i++) other = other || sp_is_str(type, known[i]); if (!rsa && !ec && !ed && !other) { - char lite[96]; - char message[200]; - int n = snprintf(message, sizeof message, - "The argument 'type' must be a supported key type. Received %s", - scr_dyn_inspect_lite(type, lite, sizeof lite)); - scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)n, "ERR_INVALID_ARG_VALUE"); + ScrStr *received = sp_received(type, false); + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", + "The argument 'type' must be a supported key type. Received %.*s", (int)received->len, + received->data); + scr_str_release(received); return NULL; } /* The ed25519 options are optional. */ @@ -1252,14 +1274,15 @@ static SpKey *sp_generate(const ScrDyn *type, const ScrDyn *options) { return NULL; } double b = bits->v.num; - if (b != b || b < 0 || b > 4294967295.0 || b != (double)(uint64_t)b) { + /* validateUint32: an integer first, then the range. */ + bool integer = isfinite(b) && trunc(b) == b; + if (!integer || b < 0 || b > 4294967295.0) { sp_key_release(k); - char message[160]; - char lite[64]; - int n = snprintf(message, sizeof message, - "The value of \"options.modulusLength\" is out of range. It must be >= 0 && <= 4294967295. Received %s", - scr_dyn_inspect_lite(bits, lite, sizeof lite)); - scr_throw_error_msg_code(SCR_ERR_RANGE, message, (size_t)n, "ERR_OUT_OF_RANGE"); + ScrStr *received = sp_received(bits, true); + scr_pki_throw_fmt(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", + "The value of \"options.modulusLength\" is out of range. It must be %s. Received %.*s", + integer ? ">= 0 && <= 4294967295" : "an integer", (int)received->len, received->data); + scr_str_release(received); return NULL; } const ScrDyn *exp = sp_opt(options, "publicExponent"); @@ -1404,10 +1427,15 @@ static bool sp_algorithm(const ScrDyn *alg, mbedtls_md_type_t *md) { return false; } if (!sp_digest_of(alg->v.str, md)) { - char message[160]; - int n = snprintf(message, sizeof message, "Invalid digest: %.*s", (int)alg->v.str->len, - alg->v.str->data); - scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)n, "ERR_CRYPTO_INVALID_DIGEST"); + /* The whole name, at any length and with any bytes, as in Node. */ + static const char prefix[] = "Invalid digest: "; + size_t len = sizeof prefix - 1 + alg->v.str->len; + char *message = malloc(len); + if (!message) scr_trap("scriptc: out of memory\n"); + memcpy(message, prefix, sizeof prefix - 1); + memcpy(message + sizeof prefix - 1, alg->v.str->data, alg->v.str->len); + scr_throw_error_msg_code(SCR_ERR_TYPE, message, len, "ERR_CRYPTO_INVALID_DIGEST"); + free(message); return false; } return true; @@ -1799,9 +1827,7 @@ static ScrDyn *sp_signer_invoke(void *h, ScrDyn *self, const char *method, ScrDy sp_key_release(k.key); return scr_dyn_new_bool(ok); } - char message[160]; - int n = snprintf(message, sizeof message, "%s is not a function", what); - scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "%s is not a function", what); return NULL; } diff --git a/packages/runtime/src/scr_pki.h b/packages/runtime/src/scr_pki.h index e299806f71..71be3c1df7 100644 --- a/packages/runtime/src/scr_pki.h +++ b/packages/runtime/src/scr_pki.h @@ -12,6 +12,10 @@ typedef struct SpKey ScrPkiKey; +/* Throws an error of `kind` (with `code`, or none for NULL) whose message + * is the printf format's whole output, at any length. */ +void scr_pki_throw_fmt(int kind, const char *code, const char *fmt, ...) __attribute__((format(printf, 3, 4))); + /* The Ed25519 arithmetic (scr_ed25519.c), installed at startup in * programs that name Ed25519; NULL otherwise. Keys are the 32-byte seed * and the 32-byte encoded public point. */ diff --git a/packages/runtime/src/scr_webcrypto_pki.c b/packages/runtime/src/scr_webcrypto_pki.c index e913b1dc6b..7439a12dbf 100644 --- a/packages/runtime/src/scr_webcrypto_pki.c +++ b/packages/runtime/src/scr_webcrypto_pki.c @@ -115,10 +115,8 @@ static bool sw_key_set(void *h, const char *name, size_t len, const ScrDyn *valu if (!sw_name_is(name, len, "type") && !sw_name_is(name, len, "extractable") && !sw_name_is(name, len, "algorithm") && !sw_name_is(name, len, "usages")) return false; - char message[128]; - int n = snprintf(message, sizeof message, "Cannot set property %.*s of # which has only a getter", - (int)len, name); - scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "Cannot set property %.*s of # which has only a getter", (int)len, + name); return true; } @@ -129,9 +127,7 @@ static ScrDyn *sw_key_invoke(void *h, ScrDyn *self, const char *method, ScrDyn * (void)args; (void)argc; if (strcmp(method, "toString") == 0) return scr_wc_cstr("[object CryptoKey]"); - char message[160]; - int n = snprintf(message, sizeof message, "%s is not a function", what); - scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "%s is not a function", what); return NULL; } diff --git a/tests/corpus/7083-crypto-sign-verify.ts b/tests/corpus/7083-crypto-sign-verify.ts index 437dc16404..4f6b86e722 100644 --- a/tests/corpus/7083-crypto-sign-verify.ts +++ b/tests/corpus/7083-crypto-sign-verify.ts @@ -80,6 +80,9 @@ const ecSignature = ecSigner.sign({ key: ec, dsaEncoding: "ieee-p1363" }); attempt("chained", () => crypto.createVerify("sha512").update("chained").verify({ key: ecPublic, dsaEncoding: "ieee-p1363" }, ecSignature)); attempt("invalid digest", () => crypto.sign("nope", data, rsa)); +const longDigest = "unknown-digest-".repeat(40); +attempt("long digest", () => crypto.sign(longDigest, data, rsa)); +attempt("long verify digest", () => crypto.verify(longDigest, data, rsaPublic, Buffer.alloc(64))); attempt("public key signs", () => crypto.sign("sha256", data, rsaPublic)); attempt("invalid createSign", () => crypto.createSign("nope")); attempt("invalid dsaEncoding", () => crypto.sign("sha256", data, { key: ec, dsaEncoding: "x" as "der" })); diff --git a/tests/corpus/7084-crypto-key-pairs.cjs b/tests/corpus/7084-crypto-key-pairs.cjs index ed4db03106..ca267d89f3 100644 --- a/tests/corpus/7084-crypto-key-pairs.cjs +++ b/tests/corpus/7084-crypto-key-pairs.cjs @@ -50,6 +50,20 @@ attempt("unknown curve", () => crypto.generateKeyPairSync("ec", { namedCurve: "n attempt("bad encoding format", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: { type: "spki", format: "xml" } })); attempt("bad encoding type", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", privateKeyEncoding: { type: "spki", format: "pem" } })); attempt("encoding not object", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: "pem" })); +// Error messages hold the whole text at any length, and received values +// render as Node's validators print them. +attempt("long type", () => crypto.generateKeyPairSync("unknown-type-".repeat(20), {})); +attempt("long encoding type", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: { type: "z".repeat(300), format: "pem" } })); +attempt("quoted encoding type", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: { type: "it's\n", format: "pem" } })); +attempt("huge modulusLength", () => crypto.generateKeyPairSync("rsa", { modulusLength: 2 ** 40 })); +attempt("fractional modulusLength", () => crypto.generateKeyPairSync("rsa", { modulusLength: 1.5 })); +attempt("NaN modulusLength", () => crypto.generateKeyPairSync("rsa", { modulusLength: NaN })); +const key = rsa.privateKey; +attempt("long method", () => key.aMethodNameLongEnoughThatTheWholeMessageRunsPastOneHundredAndSixtyBytesOnceTheReceiverTextAndTheSuffixAreIncluded()); +attempt("getter", () => { + "use strict"; + key.asymmetricKeyType = "ec"; +}); let returned = false; crypto.generateKeyPair("ec", { namedCurve: "P-384" }, (err, publicKey, privateKey) => { From 01a467060b1ee76dac71a6fc0e77bc599150ecc3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:48:30 -0500 Subject: [PATCH 112/395] Support WebCrypto wrapKey and unwrapKey crypto.subtle.wrapKey exports a secret key (raw, raw-secret or jwk) and encrypts it, and unwrapKey decrypts and imports one, as Node's webcrypto.js does: the wrap algorithm normalizes through the wrapKey registry (AES-KW) and then the encrypt registry (AES-GCM, AES-CBC and AES-CTR with their dictionaries), the wrapping key's algorithm and usages are checked, and a JWK payload is its JSON text, padded with spaces to a multiple of 8 bytes for AES-KW. A wrapped JWK that is not UTF-8 JSON with a kty member rejects with Node's DataError. AES-KW is RFC 3394 key wrap over the vendored mbedTLS block cipher, in scr_webcrypto_aes.c beside the other AES modes: wrapping takes a multiple of 8 bytes and at least 16, unwrapping a multiple of 8 and at least 24, and the integrity check value is compared in constant time. The cipher's failures, and unwrapKey's import errors, reject on a later loop turn as Node's job promises do. This completes jose's symmetric key management: 7132 runs the real jose 6.2.12 PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW, A128KW and A256KW paths (EncryptJWT and jwtDecrypt) against Node. getPublicKey replaces wrapKey as the refused method in the fenced fixture and the JavaScript refusal test. --- .../limitations/builtin-apis/crypto.mdx | 2 +- .../src/generated/node-v24-compatibility.json | 12 +- .../generated/node-v24-internal.json | 77 +- internal/compatibility/static-support.json | 4 +- .../ambient/scriptc-node-fallback.d.ts | 18 +- .../compiler/src/backend/llvm/lib-shared.ts | 4 + .../src/backend/target-diagnostics.ts | 9 +- .../frontend/lowering/builtins/webcrypto.ts | 8 +- .../src/frontend/lowering/surfaces.ts | 4 +- .../compiler/src/ir/builtin-signatures.ts | 2 + packages/compiler/src/ir/runtime-features.ts | 18 +- packages/compiler/surface-manifest.json | 2 +- .../corpus/7130-webcrypto-wrap-key.ts.json | 6 + .../7131-webcrypto-wrap-key-errors.mjs.json | 6 + .../corpus/7132-jose-pbes2/main.mjs.json | 6 + packages/runtime/src/scr_crypto.h | 7 + packages/runtime/src/scr_webcrypto.c | 317 ++++++-- packages/runtime/src/scr_webcrypto_aes.c | 67 ++ tests/corpus/7130-webcrypto-wrap-key.ts | 69 ++ .../corpus/7131-webcrypto-wrap-key-errors.mjs | 65 ++ tests/corpus/7132-jose-pbes2/main.mjs | 50 ++ .../node_modules/jose/LICENSE.md | 21 + .../jose/dist/types/jwt/decrypt.d.ts | 34 + .../jose/dist/types/jwt/encrypt.d.ts | 57 ++ .../node_modules/jose/dist/types/types.d.ts | 736 ++++++++++++++++++ .../jose/dist/types/util/errors.d.ts | 182 +++++ .../jose/dist/webapi/jwt/decrypt.js | 13 + .../jose/dist/webapi/jwt/encrypt.js | 45 ++ .../jose/dist/webapi/lib/buffer_utils.js | 75 ++ .../dist/webapi/lib/content_encryption.js | 90 +++ .../jose/dist/webapi/lib/deflate.js | 40 + .../jose/dist/webapi/lib/jwe_algorithms.js | 116 +++ .../jose/dist/webapi/lib/jwe_decrypt.js | 187 +++++ .../jose/dist/webapi/lib/jwe_encrypt.js | 75 ++ .../jose/dist/webapi/lib/jwt_claims_set.js | 146 ++++ .../node_modules/jose/dist/webapi/lib/key.js | 155 ++++ .../jose/dist/webapi/lib/key_descriptor.js | 9 + .../jose/dist/webapi/lib/key_management.js | 196 +++++ .../jose/dist/webapi/lib/validate.js | 122 +++ .../jose/dist/webapi/util/base64url.js | 16 + .../jose/dist/webapi/util/errors.js | 109 +++ .../node_modules/jose/package.json | 17 + tests/fixtures/node-types/fenced.ts | 8 +- .../__snapshots__/node-types-fenced.txt | 10 +- .../__snapshots__/webcrypto-refusals.ts.txt | 4 +- .../7130-webcrypto-wrap-key.ts.json | 1 + .../7131-webcrypto-wrap-key-errors.mjs.json | 1 + .../7132-jose-pbes2/main.mjs.json | 1 + tests/harness/errors.test.ts | 12 +- 49 files changed, 3139 insertions(+), 92 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7130-webcrypto-wrap-key.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7131-webcrypto-wrap-key-errors.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-jose-pbes2/main.mjs.json create mode 100644 tests/corpus/7130-webcrypto-wrap-key.ts create mode 100644 tests/corpus/7131-webcrypto-wrap-key-errors.mjs create mode 100644 tests/corpus/7132-jose-pbes2/main.mjs create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/LICENSE.md create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/jwt/decrypt.d.ts create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/jwt/encrypt.d.ts create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/types.d.ts create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/util/errors.d.ts create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/jwt/decrypt.js create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/jwt/encrypt.js create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/buffer_utils.js create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/content_encryption.js create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/deflate.js create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_algorithms.js create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_decrypt.js create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_encrypt.js create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwt_claims_set.js create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key.js create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key_descriptor.js create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key_management.js create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/validate.js create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/util/base64url.js create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/util/errors.js create mode 100644 tests/corpus/7132-jose-pbes2/node_modules/jose/package.json create mode 100644 tests/harness/corpus-costs/7130-webcrypto-wrap-key.ts.json create mode 100644 tests/harness/corpus-costs/7131-webcrypto-wrap-key-errors.mjs.json create mode 100644 tests/harness/corpus-costs/7132-jose-pbes2/main.mjs.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 641d0d8f93..2cb0db5f01 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -20,6 +20,6 @@ ECDH property reads and writes throw a coded SC2020 error. Other public-key operations remain unsupported. The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript. - Its `subtle` object, also node:crypto's `subtle` export, supports `generateKey()` and `exportKey()` for HMAC and AES keys, `importKey()` for HMAC, AES, PBKDF2, and HKDF keys, `encrypt()` and `decrypt()` with AES-GCM, AES-CBC, and AES-CTR on native targets, `deriveBits()` and `deriveKey()` with PBKDF2 and HKDF, HMAC `sign()` and `verify()`, and SHA-1 and SHA-2 `digest()`. + Its `subtle` object, also node:crypto's `subtle` export, supports `generateKey()` and `exportKey()` for HMAC and AES keys, `importKey()` for HMAC, AES, PBKDF2, and HKDF keys, `encrypt()` and `decrypt()` with AES-GCM, AES-CBC, and AES-CTR and `wrapKey()` and `unwrapKey()` with AES-KW or those ciphers on native targets, `deriveBits()` and `deriveKey()` with PBKDF2 and HKDF, HMAC `sign()` and `verify()`, and SHA-1 and SHA-2 `digest()`. Secret keys import and export in the `raw`, `raw-secret`, and `jwk` formats; other SubtleCrypto methods, algorithms, and key formats are refused. Native derivations finish before their deferred callbacks, so computation blocks the caller. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 682e68f994..628c267be7 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -80277,9 +80277,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 documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80323,9 +80323,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 documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index a86967ff38..83b4c56911 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -106677,6 +106677,9 @@ "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -110549,6 +110552,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.decrypt", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110587,6 +110593,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.deriveBits", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110625,6 +110634,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.deriveKey", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110663,6 +110675,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.digest", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110747,6 +110762,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.encrypt", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110785,6 +110803,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.exportKey", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110846,6 +110867,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.generateKey", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110884,6 +110908,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.importKey", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110922,6 +110949,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.sign", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -110957,8 +110987,26 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.unwrapKey", + "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110983,6 +111031,9 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.verify", "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", @@ -111018,8 +111069,26 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.wrapKey", + "tests": [ + "tests/corpus/7130-webcrypto-wrap-key.ts", + "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", + "tests/corpus/7132-jose-pbes2/main.mjs", + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 6b3eaab4c6..568dbbf0d7 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -187,14 +187,14 @@ "stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"], "stdlib.headers": ["tests/harness/fetch-conformance.test.ts"], "stdlib.request": ["tests/harness/fetch-conformance.test.ts"], - "stdlib.webcrypto.subtle": ["tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], + "stdlib.webcrypto.subtle": ["tests/corpus/7130-webcrypto-wrap-key.ts", "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], "stdlib.webcrypto.CryptoKey": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], "stdlib.response": ["tests/harness/fetch-conformance.test.ts"], "stdlib.readable-stream": ["tests/harness/fetch-conformance.test.ts"], "stdlib.abort": ["tests/harness/fetch-conformance.test.ts"] }, "features": [ - { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, + { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.wrapKey", "subtle.unwrapKey", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7130-webcrypto-wrap-key.ts", "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, { "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, { "chapter": "webcrypto", "signatures": ["Class: `Crypto`"], "status": "partial", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6293-webcrypto-digest.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 936283d407..1e40b6f6f6 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -547,7 +547,8 @@ declare var DOMException: { /* WebCrypto's secret-key slice (scr_webcrypto.c): crypto.subtle's * importKey, generateKey and exportKey for HMAC and AES keys (importKey * also for PBKDF2 and HKDF keys), AES-GCM/CBC/CTR encrypt and decrypt, - * PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as + * wrapKey and unwrapKey (AES-KW or those ciphers), PBKDF2 and HKDF + * deriveBits and deriveKey, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as * exactly that surface (the declarations mirror @types/node's webcrypto * shapes), plus the global's randomUUID and getRandomValues. Every other * SubtleCrypto method and crypto member is absent, so a use is an honest @@ -707,12 +708,27 @@ interface SubtleCrypto { keyUsages: readonly KeyUsage[], ): Promise; sign(algorithm: AlgorithmIdentifier, key: CryptoKey, data: BufferSource): Promise; + unwrapKey( + format: KeyFormat, + wrappedKey: BufferSource, + unwrappingKey: CryptoKey, + unwrapAlgorithm: AlgorithmIdentifier | AesGcmParams | AesCbcParams | AesCtrParams, + unwrappedKeyAlgorithm: AlgorithmIdentifier | HmacImportParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; verify( algorithm: AlgorithmIdentifier, key: CryptoKey, signature: BufferSource, data: BufferSource, ): Promise; + wrapKey( + format: KeyFormat, + key: CryptoKey, + wrappingKey: CryptoKey, + wrapAlgorithm: AlgorithmIdentifier | AesGcmParams | AesCbcParams | AesCtrParams, + ): Promise; } interface Crypto { readonly subtle: SubtleCrypto; diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index c08d7b720f..8805d7e0c6 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -620,6 +620,8 @@ export const LIB_FN_SYMS: Record = { "webcrypto.exportKey": "scr_webcrypto_export_key", "webcrypto.encrypt": "scr_webcrypto_encrypt", "webcrypto.decrypt": "scr_webcrypto_decrypt", + "webcrypto.wrapKey": "scr_webcrypto_wrap_key", + "webcrypto.unwrapKey": "scr_webcrypto_unwrap_key", "webcrypto.deriveBits": "scr_webcrypto_derive_bits", "webcrypto.deriveKey": "scr_webcrypto_derive_key", "webcrypto.sign": "scr_webcrypto_sign", @@ -1595,6 +1597,8 @@ export const USES_TIMERS_LIB_FNS = new Set([ "webcrypto.generateKey", "webcrypto.encrypt", "webcrypto.decrypt", + "webcrypto.wrapKey", + "webcrypto.unwrapKey", "webcrypto.deriveBits", "webcrypto.deriveKey", "webcrypto.sign", diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts index eb70a0d2c5..bd901341eb 100644 --- a/packages/compiler/src/backend/target-diagnostics.ts +++ b/packages/compiler/src/backend/target-diagnostics.ts @@ -208,10 +208,15 @@ export function moduleWasiUnavailableSurface( }; return false; } - if (node.fn === "webcrypto.encrypt" || node.fn === "webcrypto.decrypt") { + if ( + node.fn === "webcrypto.encrypt" || + node.fn === "webcrypto.decrypt" || + node.fn === "webcrypto.wrapKey" || + node.fn === "webcrypto.unwrapKey" + ) { found = { surface: - "WebCrypto encrypt and decrypt (the wasm32-wasi runtime does not include the mbedTLS library)", + "WebCrypto encrypt, decrypt, wrapKey and unwrapKey (the wasm32-wasi runtime does not include the mbedTLS library)", loc, }; return false; diff --git a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts index fe310294c3..e4f1f0955c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts @@ -9,8 +9,8 @@ import { isWebCryptoReceiver } from "./crypto.js"; /* WebCrypto's secret-key slice (scr_webcrypto.c, with encrypt and * decrypt in scr_webcrypto_aes.c): `crypto.subtle`'s `importKey`, - * `generateKey`, `exportKey`, `encrypt`, `decrypt`, `deriveBits`, - * `deriveKey`, `sign`, `verify` and `digest` + * `generateKey`, `exportKey`, `encrypt`, `decrypt`, `wrapKey`, + * `unwrapKey`, `deriveBits`, `deriveKey`, `sign`, `verify` and `digest` * called on the crypto global (also through * globalThis, node:crypto's webcrypto and node:crypto's subtle) lower to * runtime entry points that take the argument vector as one dyn array and @@ -25,6 +25,8 @@ const SUBTLE_LOWERED: ReadonlyMap = new Map([ ["exportKey", "webcrypto.exportKey"], ["encrypt", "webcrypto.encrypt"], ["decrypt", "webcrypto.decrypt"], + ["wrapKey", "webcrypto.wrapKey"], + ["unwrapKey", "webcrypto.unwrapKey"], ["deriveBits", "webcrypto.deriveBits"], ["deriveKey", "webcrypto.deriveKey"], ["sign", "webcrypto.sign"], @@ -33,7 +35,7 @@ const SUBTLE_LOWERED: ReadonlyMap = new Map([ ]); const SUBTLE_HINT = - "the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; + "the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, wrapKey and unwrapKey with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; /** Whether `node` is a SubtleCrypto object: `subtle` on a WebCrypto * Crypto object (the global, globalThis.crypto or node:crypto's diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index bb9a6c0625..6179f8c783 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2216,13 +2216,15 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ "webcrypto.exportKey", "webcrypto.encrypt", "webcrypto.decrypt", + "webcrypto.wrapKey", + "webcrypto.unwrapKey", "webcrypto.deriveBits", "webcrypto.deriveKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest", ], - note: "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", + note: "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, wrapKey and unwrapKey with AES-KW or those ciphers, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", }, { id: "stdlib.webcrypto.CryptoKey", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 8f5928d12d..380c7cb7ce 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2862,6 +2862,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "webcrypto.exportKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.encrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.decrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.wrapKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.unwrapKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.deriveBits": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.deriveKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.sign": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 9531d78456..5c60eb54f4 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -650,9 +650,17 @@ const RUNTIME_FEATURE_RULES = { }), /** scr_cipher.c over the mbedTLS archive; DEP0182 reports through the * process-warning unit, and unknown output encodings render through the - * inspect unit. WebCrypto's encrypt and decrypt (scr_webcrypto_aes.c) - * link the same archive. */ - cipher: rule({ fns: ["crypto.cipherNew", "webcrypto.encrypt", "webcrypto.decrypt"] }), + * inspect unit. WebCrypto's encrypt, decrypt, wrapKey and unwrapKey + * (scr_webcrypto_aes.c) link the same archive. */ + cipher: rule({ + fns: [ + "crypto.cipherNew", + "webcrypto.encrypt", + "webcrypto.decrypt", + "webcrypto.wrapKey", + "webcrypto.unwrapKey", + ], + }), copying: rule({ expr: (node, _tables) => (node.kind === "arrIntrinsic" && COPYING_ARR_METHODS.has(node.method)) || @@ -688,6 +696,8 @@ const RUNTIME_FEATURE_RULES = { "crypto.cipherNew", "webcrypto.encrypt", "webcrypto.decrypt", + "webcrypto.wrapKey", + "webcrypto.unwrapKey", "crypto.ecdhNew", ], prefixes: [...READABLE_DYN_ASYNC_PREFIXES, "wasm."], @@ -796,6 +806,8 @@ const RUNTIME_FEATURE_RULES = { "crypto.cipherNew", "webcrypto.encrypt", "webcrypto.decrypt", + "webcrypto.wrapKey", + "webcrypto.unwrapKey", "crypto.ecdhNew", "crypto.ecdhConvertKey", ...BUILTIN_CLASS_LIB_FNS, diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 3e8b9df731..02b1181b51 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -5718,7 +5718,7 @@ "kind": "stdlib", "name": "globalThis.crypto.subtle", "status": "static", - "note": "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" + "note": "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, wrapKey and unwrapKey with AES-KW or those ciphers, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" }, { "id": "syntax.async-generators", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7130-webcrypto-wrap-key.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7130-webcrypto-wrap-key.ts.json new file mode 100644 index 0000000000..fa5b5529ff --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7130-webcrypto-wrap-key.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7130-webcrypto-wrap-key.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7131-webcrypto-wrap-key-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7131-webcrypto-wrap-key-errors.mjs.json new file mode 100644 index 0000000000..275ffa4180 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7131-webcrypto-wrap-key-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7131-webcrypto-wrap-key-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-jose-pbes2/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-jose-pbes2/main.mjs.json new file mode 100644 index 0000000000..9fbd24555c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7132-jose-pbes2/main.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7132-jose-pbes2/main.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index ddc306598b..99d8b85bc9 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -241,6 +241,13 @@ typedef ScrBytes *(*ScrWebCryptoAesFn)(const char *name, bool decrypt, const uns ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn aes); ScrPromise *scr_webcrypto_encrypt(ScrDyn *args); ScrPromise *scr_webcrypto_decrypt(ScrDyn *args); +/* wrapKey and unwrapKey run the same way (AES-KW wrapping, or the AES + * ciphers): wrapKey fulfills with an ArrayBuffer and unwrapKey with a + * CryptoKey on a later loop turn. */ +ScrPromise *scr_webcrypto_wrap(ScrDyn *args, ScrWebCryptoAesFn aes); +ScrPromise *scr_webcrypto_unwrap(ScrDyn *args, ScrWebCryptoAesFn aes); +ScrPromise *scr_webcrypto_wrap_key(ScrDyn *args); +ScrPromise *scr_webcrypto_unwrap_key(ScrDyn *args); /* deriveBits and deriveKey (PBKDF2 and HKDF) over node:crypto's KDF core: * deriveBits fulfills with an ArrayBuffer (at once for a zero length) and * deriveKey with a CryptoKey of the derived key type, on a later loop diff --git a/packages/runtime/src/scr_webcrypto.c b/packages/runtime/src/scr_webcrypto.c index 4d2c27be42..fc6941e36b 100644 --- a/packages/runtime/src/scr_webcrypto.c +++ b/packages/runtime/src/scr_webcrypto.c @@ -104,7 +104,7 @@ static const char *scr_wc_native_hash(const char *name) { /* ── errors ────────────────────────────────────────────────────────── */ -static const char *const scr_wc_ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"}; +static const char *const scr_wc_ordinals[] = {"1st", "2nd", "3rd", "4th", "5th", "6th", "7th"}; static void scr_wc_type_error(const char *code, const char *fmt, ...) __attribute__((format(printf, 2, 3))); @@ -1151,6 +1151,47 @@ ScrPromise *scr_webcrypto_generate_key(ScrDyn *args) { return scr_wc_deferred(key); } +/* exportKeySync's checks for a secret key: false with Node's error + * pending. raw, raw-secret and jwk are the formats a secret key exports. */ +static bool scr_wc_exportable(const char *format, const ScrCryptoKey *key) { + if (scr_wc_is_kdf(key->name)) { + char message[64]; + snprintf(message, sizeof message, "%s key export is not supported", key->name); + scr_throw_domex("NotSupportedError", message); + return false; + } + if (!key->extractable) { + scr_throw_domex("InvalidAccessError", "key is not extractable"); + return false; + } + if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0 && + strcmp(format, "jwk") != 0) { + char message[96]; + snprintf(message, sizeof message, "Unable to export %s secret key using %s format", key->name, + format); + scr_throw_domex("NotSupportedError", message); + return false; + } + return true; +} + +/* The key bytes as a fresh u8 view (+1). */ +static ScrBytes *scr_wc_key_bytes(const ScrCryptoKey *key) { + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)key->len); + if (key->len) memcpy(bytes->data, key->data, key->len); + return bytes; +} + +/* A JWK's k: the key bytes in unpadded base64url (+1). */ +static ScrStr *scr_wc_jwk_k(const ScrCryptoKey *key) { + ScrBytes *bytes = scr_wc_key_bytes(key); + ScrStr *encoding = scr_str_new("base64url", 9); + ScrStr *k = scr_bytes_to_str(bytes, encoding); + scr_str_release(encoding); + scr_bytes_release(bytes); + return k; +} + /* exportKeySync for secret keys: raw and raw-secret answer an ArrayBuffer * of the key bytes, jwk an object { key_ops, ext, alg, kty, k }. */ static ScrDyn *scr_wc_export(const ScrDyn *args) { @@ -1166,30 +1207,11 @@ static ScrDyn *scr_wc_export(const ScrDyn *args) { scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); goto done; } - if (scr_wc_is_kdf(key->name)) { - char message[64]; - snprintf(message, sizeof message, "%s key export is not supported", key->name); - scr_throw_domex("NotSupportedError", message); - goto done; - } - if (!key->extractable) { - scr_throw_domex("InvalidAccessError", "key is not extractable"); - goto done; - } - if (strcmp(format, "raw") == 0 || strcmp(format, "raw-secret") == 0) { - ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)key->len); - if (key->len) memcpy(bytes->data, key->data, key->len); - result = scr_array_buffer_from_bytes(bytes); - scr_bytes_release(bytes); - } else if (strcmp(format, "jwk") == 0) { + if (!scr_wc_exportable(format, key)) goto done; + if (strcmp(format, "jwk") == 0) { char buf[16]; const char *alg = scr_wc_jwk_alg(key->name, key->hash, key->length, buf, sizeof buf); - ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)key->len); - if (key->len) memcpy(bytes->data, key->data, key->len); - ScrStr *encoding = scr_str_new("base64url", 9); - ScrStr *k = scr_bytes_to_str(bytes, encoding); - scr_str_release(encoding); - scr_bytes_release(bytes); + ScrStr *k = scr_wc_jwk_k(key); result = scr_dyn_new_obj(); scr_dyn_obj_set(result, "key_ops", 7, scr_wc_usage_array(key->usages)); scr_dyn_obj_set(result, "ext", 3, scr_dyn_new_bool(key->extractable)); @@ -1198,10 +1220,9 @@ static ScrDyn *scr_wc_export(const ScrDyn *args) { scr_dyn_obj_set(result, "k", 1, scr_dyn_new_str(k)); scr_str_release(k); } else { - char message[96]; - snprintf(message, sizeof message, "Unable to export %s secret key using %s format", key->name, - format); - scr_throw_domex("NotSupportedError", message); + ScrBytes *bytes = scr_wc_key_bytes(key); + result = scr_array_buffer_from_bytes(bytes); + scr_bytes_release(bytes); } done: scr_wc_args_drop(&a, 2); @@ -1548,6 +1569,43 @@ static ScrPromise *scr_wc_deferred_buffer(ScrBytes *bytes) { return p; } +/* The cipher over `data` (cipherOrWrap's job): +1 output bytes, or NULL + * with Node's OperationError pending. */ +static ScrBytes *scr_wc_cipher_bytes(ScrWebCryptoAesFn aes, const char *name, bool decrypt, + const ScrCryptoKey *key, ScrWcCipherParams *params, + ScrBytes *data) { + if (!params->iv) params->iv = scr_bytes_new(SCR_BYTES_U8, 0); + if (!params->aad) params->aad = scr_bytes_new(SCR_BYTES_U8, 0); + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + ScrBytes *iv = params->iv; + SCR_BYTES_SNAPSHOT(iv); + ScrBytes *aad = params->aad; + SCR_BYTES_SNAPSHOT(aad); + ScrWebCryptoAesParams run = { + .iv = iv->data, + .iv_len = (size_t)scr_bytes_byte_len(iv), + .aad = aad->data, + .aad_len = (size_t)scr_bytes_byte_len(aad), + .tag_len = (size_t)(params->tag_bits / 8), + .counter_bits = (unsigned)params->counter_bits, + }; + ScrBytes *out = + aes(name, decrypt, key->data, key->len, &run, view->data, (size_t)scr_bytes_byte_len(view)); + if (!out) + scr_throw_domex("OperationError", "The operation failed for an operation-specific reason"); + return out; +} + +/* The cipher's promise: an ArrayBuffer of the output, or the job's + * OperationError, on a later loop turn. */ +static ScrPromise *scr_wc_run_cipher(ScrWebCryptoAesFn aes, const char *name, bool decrypt, + const ScrCryptoKey *key, ScrWcCipherParams *params, + ScrBytes *data) { + ScrBytes *out = scr_wc_cipher_bytes(aes, name, decrypt, key, params, data); + return out ? scr_wc_deferred_buffer(out) : scr_wc_deferred_rejection(); +} + ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn aes) { const char *method = decrypt ? "decrypt" : "encrypt"; const char *prefix = decrypt ? "Failed to execute 'decrypt' on 'SubtleCrypto'" @@ -1580,31 +1638,7 @@ ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn a goto done; } if (!scr_wc_max_length(data, "data")) goto done; - { - ScrBytes *view = data; - SCR_BYTES_SNAPSHOT(view); - ScrBytes *iv = params.iv; - SCR_BYTES_SNAPSHOT(iv); - if (!params.aad) params.aad = scr_bytes_new(SCR_BYTES_U8, 0); - ScrBytes *aad = params.aad; - SCR_BYTES_SNAPSHOT(aad); - ScrWebCryptoAesParams run = { - .iv = iv->data, - .iv_len = (size_t)scr_bytes_byte_len(iv), - .aad = aad->data, - .aad_len = (size_t)scr_bytes_byte_len(aad), - .tag_len = (size_t)(params.tag_bits / 8), - .counter_bits = (unsigned)params.counter_bits, - }; - ScrBytes *out = aes(name, decrypt, key->data, key->len, &run, view->data, - (size_t)scr_bytes_byte_len(view)); - if (out) { - result = scr_wc_deferred_buffer(out); - } else { - scr_throw_domex("OperationError", "The operation failed for an operation-specific reason"); - result = scr_wc_deferred_rejection(); - } - } + result = scr_wc_run_cipher(aes, name, decrypt, key, ¶ms, data); done: if (data) scr_bytes_release(data); scr_wc_cipher_params_drop(¶ms); @@ -1612,6 +1646,185 @@ ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn a return result ? result : scr_wc_rejected(); } +/* ── wrapKey and unwrapKey ──────────────────────────────────────────── + * wrapKey is exportKey followed by encrypt (or AES-KW's wrap), and + * unwrapKey is decrypt (or AES-KW's unwrap) followed by importKey, as in + * Node's webcrypto.js. The cipher runs in scr_webcrypto_aes.c. */ + +/* normalizeAlgorithm(algorithm, 'wrapKey' / 'unwrapKey'), falling back to + * the 'encrypt' / 'decrypt' registry as Node does: AES-KW (no + * parameters), or the AES ciphers with their dictionaries. */ +static const char *scr_wc_normalize_wrap(ScrDyn *identifier, const char *method, + ScrWcCipherParams *params) { + memset(params, 0, sizeof *params); + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return NULL; + bool kw = scr_wc_ascii_ieq(name, "AES-KW"); + scr_str_release(name); + if (kw) return "AES-KW"; + return scr_wc_normalize_cipher(identifier, method, params); +} + +/* The bytes wrapKey encrypts (+1): the raw key, or the JWK's JSON text, + * padded with spaces to a multiple of 8 bytes for AES-KW. */ +static ScrBytes *scr_wc_wrap_payload(const char *format, const ScrCryptoKey *key, bool kw) { + if (strcmp(format, "jwk") != 0) return scr_wc_key_bytes(key); + char buf[16]; + const char *alg = scr_wc_jwk_alg(key->name, key->hash, key->length, buf, sizeof buf); + ScrStr *k = scr_wc_jwk_k(key); + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "{\"key_ops\":["); + bool first = true; + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) { + if (!(key->usages & (1u << i))) continue; + if (!first) scr_jb_puts(&b, ","); + first = false; + scr_jb_puts(&b, "\""); + scr_jb_puts(&b, scr_wc_usages[i]); + scr_jb_puts(&b, "\""); + } + scr_jb_puts(&b, key->extractable ? "],\"ext\":true" : "],\"ext\":false"); + if (alg) { + scr_jb_puts(&b, ",\"alg\":\""); + scr_jb_puts(&b, alg); + scr_jb_puts(&b, "\""); + } + scr_jb_puts(&b, ",\"kty\":\"oct\",\"k\":\""); + ScrStr *json_prefix = scr_jb_finish(&b); + size_t len = json_prefix->len + k->len + 2; + size_t padded = kw && len % 8 ? len + 8 - len % 8 : len; + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)padded); + memcpy(out->data, json_prefix->data, json_prefix->len); + memcpy(out->data + json_prefix->len, k->data, k->len); + memcpy(out->data + json_prefix->len + k->len, "\"}", 2); + memset(out->data + len, ' ', padded - len); + scr_str_release(json_prefix); + scr_str_release(k); + return out; +} + +/* wrapKey(format, key, wrappingKey, wrapAlgorithm). */ +ScrPromise *scr_webcrypto_wrap(ScrDyn *args, ScrWebCryptoAesFn aes) { + static const char prefix[] = "Failed to execute 'wrapKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 4)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 4); + ScrWcCipherParams params; + memset(¶ms, 0, sizeof params); + ScrBytes *payload = NULL; + ScrPromise *result = NULL; + const char *format = scr_wc_key_format(prefix, a.v[0]); + if (!format) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + ScrCryptoKey *wrapping = scr_crypto_key_of(a.v[2]); + if (!wrapping) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 3rd argument is not of type CryptoKey.", prefix); + goto done; + } + if (!scr_wc_algorithm_identifier(a.v[3])) goto done; + const char *name = scr_wc_normalize_wrap(a.v[3], "wrapKey", ¶ms); + if (!name) goto done; + if (strcmp(wrapping->name, name) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + goto done; + } + if (!(wrapping->usages & SCR_WC_USAGE_WRAP_KEY)) { + scr_throw_domex("InvalidAccessError", "Unable to use this key to wrapKey"); + goto done; + } + if (!scr_wc_exportable(format, key)) goto done; + payload = scr_wc_wrap_payload(format, key, strcmp(name, "AES-KW") == 0); + result = scr_wc_run_cipher(aes, name, false, wrapping, ¶ms, payload); +done: + if (payload) scr_bytes_release(payload); + scr_wc_cipher_params_drop(¶ms); + scr_wc_args_drop(&a, 4); + return result ? result : scr_wc_rejected(); +} + +/* parseJwk: the unwrapped bytes as UTF-8 JSON converted to a JsonWebKey + * with a kty member, or DataError "Invalid wrapped JWK key". */ +static bool scr_wc_parse_wrapped_jwk(ScrBytes *bytes, ScrWcJwk *jwk) { + static const char prefix[] = "Failed to execute 'unwrapKey' on 'SubtleCrypto'"; + ScrStr *text = scr_text_decode_options(bytes, true, false); + ScrDyn *parsed = text ? scr_json_parse(text) : NULL; + if (text) scr_str_release(text); + bool ok = parsed && scr_wc_is_object(parsed) && scr_wc_jwk_convert(prefix, parsed, jwk) && jwk->kty; + if (parsed) scr_dyn_release(parsed); + if (!ok) { + if (scr_exc_pending()) scr_caught_release(scr_exc_take()); + scr_wc_jwk_drop(jwk); + scr_throw_domex("DataError", "Invalid wrapped JWK key"); + } + return ok; +} + +/* unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgorithm, + * unwrappedKeyAlgorithm, extractable, keyUsages): argument and key errors + * reject at once; the cipher's failure and the import's errors reject on + * a later loop turn, after Node's job. */ +ScrPromise *scr_webcrypto_unwrap(ScrDyn *args, ScrWebCryptoAesFn aes) { + static const char prefix[] = "Failed to execute 'unwrapKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 7)) return scr_wc_rejected(); + ScrDyn *v[7]; + for (size_t i = 0; i < 7; i++) v[i] = scr_dyn_arr_at(args, (double)i); + ScrWcCipherParams params; + memset(¶ms, 0, sizeof params); + ScrBytes *wrapped = NULL; + ScrPromise *result = NULL; + const char *format = scr_wc_key_format(prefix, v[0]); + if (!format) goto done; + wrapped = scr_wc_buffer_source(prefix, 1, v[1]); + if (!wrapped) goto done; + ScrCryptoKey *unwrapping = scr_crypto_key_of(v[2]); + if (!unwrapping) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 3rd argument is not of type CryptoKey.", prefix); + goto done; + } + if (!scr_wc_algorithm_identifier(v[3]) || !scr_wc_algorithm_identifier(v[4])) goto done; + bool extractable = scr_dyn_truthy(v[5]); + long usages = scr_wc_key_usages(prefix, 6, v[6]); + if (usages < 0) goto done; + const char *name = scr_wc_normalize_wrap(v[3], "unwrapKey", ¶ms); + if (!name) goto done; + ScrWcAlgorithm imported; + if (!scr_wc_normalize(v[4], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), + SCR_WC_OP_IMPORT, &imported)) + goto done; + if (strcmp(unwrapping->name, name) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + goto done; + } + if (!(unwrapping->usages & SCR_WC_USAGE_UNWRAP_KEY)) { + scr_throw_domex("InvalidAccessError", "Unable to use this key to unwrapKey"); + goto done; + } + if (!scr_wc_max_length(wrapped, "data")) goto done; + ScrBytes *bytes = scr_wc_cipher_bytes(aes, name, true, unwrapping, ¶ms, wrapped); + if (!bytes) { + result = scr_wc_deferred_rejection(); + goto done; + } + ScrWcJwk jwk; + memset(&jwk, 0, sizeof jwk); + ScrDyn *key = NULL; + if (strcmp(format, "jwk") != 0 || scr_wc_parse_wrapped_jwk(bytes, &jwk)) + key = scr_wc_import_key_sync(format, bytes, &jwk, &imported, extractable, usages); + scr_wc_jwk_drop(&jwk); + scr_bytes_release(bytes); + result = key ? scr_wc_deferred(key) : scr_wc_deferred_rejection(); +done: + if (wrapped) scr_bytes_release(wrapped); + scr_wc_cipher_params_drop(¶ms); + for (size_t i = 0; i < 7; i++) scr_dyn_release(v[i]); + return result ? result : scr_wc_rejected(); +} + /* ── deriveBits and deriveKey ───────────────────────────────────────── * PBKDF2 and HKDF over node:crypto's KDF core (scr_crypto_pbkdf2 and * scr_crypto_hkdf_bytes in scr_lib.c), with Node's Pbkdf2Params and diff --git a/packages/runtime/src/scr_webcrypto_aes.c b/packages/runtime/src/scr_webcrypto_aes.c index 7cb63f561f..016ac57af9 100644 --- a/packages/runtime/src/scr_webcrypto_aes.c +++ b/packages/runtime/src/scr_webcrypto_aes.c @@ -122,11 +122,74 @@ static ScrBytes *scr_wc_aes_ctr(const unsigned char *key, size_t key_len, return out; } +/* RFC 3394 AES key wrap with the default IV: wrapping takes a multiple of + * 8 bytes, at least 16; unwrapping a multiple of 8, at least 24, and fails + * when the integrity check value does not match (compared in constant + * time). */ +static ScrBytes *scr_wc_aes_kw(bool unwrap, const unsigned char *key, size_t key_len, + const unsigned char *in, size_t len) { + static const unsigned char iv[8] = {0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6}; + if (len % 8 != 0 || len < (unwrap ? 24u : 16u)) return NULL; + size_t n = unwrap ? len / 8 - 1 : len / 8; + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + int rc = unwrap ? mbedtls_aes_setkey_dec(&aes, key, (unsigned)(key_len * 8)) + : mbedtls_aes_setkey_enc(&aes, key, (unsigned)(key_len * 8)); + if (rc != 0) { + mbedtls_aes_free(&aes); + return NULL; + } + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)(unwrap ? len - 8 : len + 8)); + unsigned char a[8], block[16]; + unsigned char *r = unwrap ? out->data : out->data + 8; + if (unwrap) { + memcpy(a, in, 8); + memcpy(r, in + 8, len - 8); + for (int j = 5; j >= 0; j--) { + for (size_t i = n; i >= 1; i--) { + uint64_t t = (uint64_t)n * (uint64_t)j + i; + for (int k = 0; k < 8; k++) block[k] = (unsigned char)(a[k] ^ (t >> (56 - 8 * k))); + memcpy(block + 8, r + 8 * (i - 1), 8); + mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_DECRYPT, block, block); + memcpy(a, block, 8); + memcpy(r + 8 * (i - 1), block + 8, 8); + } + } + } else { + memcpy(a, iv, 8); + memcpy(r, in, len); + for (int j = 0; j <= 5; j++) { + for (size_t i = 1; i <= n; i++) { + memcpy(block, a, 8); + memcpy(block + 8, r + 8 * (i - 1), 8); + mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, block, block); + uint64_t t = (uint64_t)n * (uint64_t)j + i; + for (int k = 0; k < 8; k++) a[k] = (unsigned char)(block[k] ^ (t >> (56 - 8 * k))); + memcpy(r + 8 * (i - 1), block + 8, 8); + } + } + memcpy(out->data, a, 8); + } + mbedtls_platform_zeroize(block, sizeof block); + mbedtls_aes_free(&aes); + if (unwrap) { + unsigned char diff = 0; + for (int k = 0; k < 8; k++) diff |= (unsigned char)(a[k] ^ iv[k]); + if (diff) { + mbedtls_platform_zeroize(out->data, len - 8); + scr_bytes_release(out); + return NULL; + } + } + return out; +} + /* The cipher scr_webcrypto_cipher runs: +1 output bytes, or NULL when the * operation fails (Node's OperationError). */ static ScrBytes *scr_wc_aes(const char *name, bool decrypt, const unsigned char *key, size_t key_len, const ScrWebCryptoAesParams *p, const unsigned char *in, size_t len) { + if (strcmp(name, "AES-KW") == 0) return scr_wc_aes_kw(decrypt, key, key_len, in, len); if (strcmp(name, "AES-GCM") == 0) return scr_wc_aes_gcm(decrypt, key, key_len, p, in, len); if (strcmp(name, "AES-CBC") == 0) return scr_wc_aes_cbc(decrypt, key, key_len, p, in, len); return scr_wc_aes_ctr(key, key_len, p, in, len); @@ -135,3 +198,7 @@ static ScrBytes *scr_wc_aes(const char *name, bool decrypt, const unsigned char ScrPromise *scr_webcrypto_encrypt(ScrDyn *args) { return scr_webcrypto_cipher(args, false, scr_wc_aes); } ScrPromise *scr_webcrypto_decrypt(ScrDyn *args) { return scr_webcrypto_cipher(args, true, scr_wc_aes); } + +ScrPromise *scr_webcrypto_wrap_key(ScrDyn *args) { return scr_webcrypto_wrap(args, scr_wc_aes); } + +ScrPromise *scr_webcrypto_unwrap_key(ScrDyn *args) { return scr_webcrypto_unwrap(args, scr_wc_aes); } diff --git a/tests/corpus/7130-webcrypto-wrap-key.ts b/tests/corpus/7130-webcrypto-wrap-key.ts new file mode 100644 index 0000000000..8cf51fab47 --- /dev/null +++ b/tests/corpus/7130-webcrypto-wrap-key.ts @@ -0,0 +1,69 @@ +// crypto.subtle.wrapKey and unwrapKey: AES-KW wrapping matches RFC 3394's +// test vectors, raw and JSON Web Key payloads round-trip (AES-KW pads the +// JWK's JSON with spaces to a multiple of 8 bytes), and AES-GCM, AES-CBC +// and AES-CTR wrap keys the way encrypt does. jose's A128KW and +// PBES2-HS256+A128KW content-key wrapping is the shape used here. +const encoder = new TextEncoder(); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +type Key = Awaited>; + +function describe(key: Key): string { + return JSON.stringify({ algorithm: key.algorithm, usages: key.usages, extractable: key.extractable }); +} + +// RFC 3394 section 4: 128-, 192- and 256-bit key data under each KEK size. +const vectors: [string, string, string][] = [ + ["000102030405060708090A0B0C0D0E0F", "00112233445566778899AABBCCDDEEFF", "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5"], + ["000102030405060708090A0B0C0D0E0F1011121314151617", "00112233445566778899AABBCCDDEEFF", "96778b25ae6ca435f92b5b97c050aed2468ab8a17ad84e5d"], + ["000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", "00112233445566778899AABBCCDDEEFF", "64e8c3f9ce0f5ba263e9777905818a2a93c8191e7d6e8ae7"], + ["000102030405060708090A0B0C0D0E0F1011121314151617", "00112233445566778899AABBCCDDEEFF0001020304050607", "031d33264e15d33268f24ec260743edce1c6c7ddee725a936ba814915c6762d2"], + ["000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", "00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F", "28c9f404c4b810f4cbccb35cfb87f8263f5786e2d80ed326cbc7f0e71a99f43bfb988b9b7a02dd21"], +]; +for (const [kekHex, dataHex, expected] of vectors) { + const kek = await crypto.subtle.importKey("raw", Buffer.from(kekHex, "hex"), "AES-KW", false, ["wrapKey", "unwrapKey"]); + const data = await crypto.subtle.importKey("raw", Buffer.from(dataHex, "hex"), "AES-CBC", true, ["encrypt"]); + const wrapped = await crypto.subtle.wrapKey("raw", data, kek, "AES-KW"); + console.log(hex(wrapped) === expected, hex(wrapped)); + const unwrapped = await crypto.subtle.unwrapKey("raw", wrapped, kek, { name: "aes-kw" }, "AES-CBC", true, ["decrypt"]); + console.log(describe(unwrapped), hex(await crypto.subtle.exportKey("raw", unwrapped))); +} + +// jose's content-key wrap: an HMAC key wrapped raw, then unwrapped and exported. +const kek = await crypto.subtle.importKey("raw", new Uint8Array(16).fill(7), "AES-KW", true, ["wrapKey", "unwrapKey"]); +const cek = await crypto.subtle.importKey("raw", new Uint8Array(32).fill(9), { name: "HMAC", hash: "SHA-256" }, true, ["sign"]); +const wrappedCek = await crypto.subtle.wrapKey("raw", cek, kek, "AES-KW"); +console.log(hex(wrappedCek)); +const back = await crypto.subtle.unwrapKey("raw", wrappedCek, kek, "AES-KW", { hash: "SHA-256", name: "HMAC" }, true, ["sign"]); +console.log(describe(back), hex(await crypto.subtle.exportKey("raw", back))); + +// JSON Web Key payloads, padded for AES-KW. +const wrappedJwk = await crypto.subtle.wrapKey("jwk", cek, kek, "AES-KW"); +console.log(wrappedJwk.byteLength, hex(wrappedJwk)); +const fromJwk = await crypto.subtle.unwrapKey("jwk", wrappedJwk, kek, "AES-KW", { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +console.log(describe(fromJwk)); +const asRaw = await crypto.subtle.unwrapKey("raw", wrappedJwk, kek, "AES-KW", { name: "HMAC", hash: "SHA-256" }, true, ["sign"]); +const padded = new TextDecoder().decode(await crypto.subtle.exportKey("raw", asRaw)); +console.log(JSON.stringify(padded), padded.length); + +// The AES ciphers as wrapping algorithms. +const gcmKey = await crypto.subtle.importKey("raw", new Uint8Array(16).fill(3), "AES-GCM", false, ["wrapKey", "unwrapKey"]); +const iv = new Uint8Array(12).fill(1); +const gcmWrapped = await crypto.subtle.wrapKey("jwk", cek, gcmKey, { name: "AES-GCM", iv }); +console.log(gcmWrapped.byteLength, hex(gcmWrapped).slice(0, 64)); +console.log(describe(await crypto.subtle.unwrapKey("jwk", gcmWrapped, gcmKey, { name: "AES-GCM", iv }, { name: "HMAC", hash: "SHA-256" }, true, ["sign"]))); +const cbcKey = await crypto.subtle.importKey("raw", new Uint8Array(32).fill(4), "AES-CBC", false, ["wrapKey", "unwrapKey"]); +const cbcWrapped = await crypto.subtle.wrapKey("raw", cek, cbcKey, { name: "AES-CBC", iv: new Uint8Array(16) }); +console.log(hex(cbcWrapped)); +console.log(describe(await crypto.subtle.unwrapKey("raw", cbcWrapped, cbcKey, { name: "AES-CBC", iv: new Uint8Array(16) }, "AES-GCM", false, ["encrypt"]))); +const ctrKey = await crypto.subtle.importKey("raw", new Uint8Array(24).fill(5), "AES-CTR", false, ["wrapKey", "unwrapKey"]); +const ctr = { name: "AES-CTR", counter: new Uint8Array(16), length: 64 }; +const ctrWrapped = await crypto.subtle.wrapKey("raw", cek, ctrKey, ctr); +console.log(hex(ctrWrapped)); +const fromCtr = await crypto.subtle.unwrapKey("raw", ctrWrapped, ctrKey, ctr, { name: "HMAC", hash: "SHA-1" }, true, ["verify"]); +console.log(describe(fromCtr)); +const signature = await crypto.subtle.sign("HMAC", back, encoder.encode("message")); +console.log(hex(signature).slice(0, 32)); diff --git a/tests/corpus/7131-webcrypto-wrap-key-errors.mjs b/tests/corpus/7131-webcrypto-wrap-key-errors.mjs new file mode 100644 index 0000000000..23cb3e10d4 --- /dev/null +++ b/tests/corpus/7131-webcrypto-wrap-key-errors.mjs @@ -0,0 +1,65 @@ +// Rejections of crypto.crypto.subtle.wrapKey and unwrapKey in Node's order: +// argument conversion, wrap algorithm normalization (AES-KW, then the +// encrypt registry), the wrapping key's algorithm and usages, the export +// or import of the wrapped key, and the cipher's OperationErrors (AES-KW's +// length rules and integrity check), which reject on a later turn. + +async function attempt(label, run) { + try { + const value = await run(); + const shown = value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value instanceof CryptoKey ? JSON.stringify([value.algorithm, value.usages]) : value; + console.log(label, "resolves", shown); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +const kek = await crypto.subtle.importKey("raw", new Uint8Array(16).fill(1), "AES-KW", true, ["wrapKey", "unwrapKey"]); +const unwrapOnly = await crypto.subtle.importKey("raw", new Uint8Array(16).fill(1), "AES-KW", true, ["unwrapKey"]); +const gcm = await crypto.subtle.importKey("raw", new Uint8Array(16), "AES-GCM", true, ["wrapKey", "unwrapKey"]); +const hmac20 = await crypto.subtle.importKey("raw", new Uint8Array(20).fill(2), { name: "HMAC", hash: "SHA-256" }, true, ["sign"]); +const hmac32 = await crypto.subtle.importKey("raw", new Uint8Array(32).fill(2), { name: "HMAC", hash: "SHA-256" }, true, ["sign"]); +const locked = await crypto.subtle.importKey("raw", new Uint8Array(16), "AES-CBC", false, ["encrypt"]); +const password = await crypto.subtle.importKey("raw", new Uint8Array(4), "PBKDF2", false, ["deriveBits"]); +const hs256 = { name: "HMAC", hash: "SHA-256" }; + +await attempt("wrap too few", () => crypto.subtle.wrapKey("raw", hmac32, kek)); +await attempt("wrap bad format", () => crypto.subtle.wrapKey("pem", hmac32, kek, "AES-KW")); +await attempt("wrap key not a key", () => crypto.subtle.wrapKey("raw", JSON.parse("{}"), kek, "AES-KW")); +await attempt("wrap wrapping key not a key", () => crypto.subtle.wrapKey("raw", hmac32, JSON.parse("{}"), "AES-KW")); +await attempt("wrap unknown algorithm", () => crypto.subtle.wrapKey("raw", hmac32, kek, "AES-XTS")); +await attempt("wrap no name", () => crypto.subtle.wrapKey("raw", hmac32, kek, {})); +await attempt("wrap gcm without iv", () => crypto.subtle.wrapKey("raw", hmac32, gcm, "AES-GCM")); +await attempt("wrap mismatch", () => crypto.subtle.wrapKey("raw", hmac32, gcm, "AES-KW")); +await attempt("wrap ctr on gcm key", () => crypto.subtle.wrapKey("raw", hmac32, gcm, { name: "AES-CTR", counter: new Uint8Array(16), length: 64 })); +await attempt("wrap missing usage", () => crypto.subtle.wrapKey("raw", hmac32, unwrapOnly, "AES-KW")); +await attempt("wrap pbkdf2 key", () => crypto.subtle.wrapKey("raw", password, kek, "AES-KW")); +await attempt("wrap non-extractable", () => crypto.subtle.wrapKey("raw", locked, kek, "AES-KW")); +await attempt("wrap spki", () => crypto.subtle.wrapKey("spki", hmac32, kek, "AES-KW")); +await attempt("wrap 20 bytes with AES-KW", () => crypto.subtle.wrapKey("raw", hmac20, kek, "AES-KW")); +await attempt("wrap 20 bytes with AES-GCM", () => crypto.subtle.wrapKey("raw", hmac20, gcm, { name: "AES-GCM", iv: new Uint8Array(12) })); + +const wrapped = new Uint8Array(await crypto.subtle.wrapKey("raw", hmac32, kek, "AES-KW")); +const tampered = wrapped.slice(); +tampered[3] ^= 1; +const notJson = await crypto.subtle.wrapKey("raw", hmac32, kek, "AES-KW"); +await attempt("unwrap too few", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", hs256, true)); +await attempt("unwrap data string", () => crypto.subtle.unwrapKey("raw", "x", kek, "AES-KW", hs256, true, ["sign"])); +await attempt("unwrap not a key", () => crypto.subtle.unwrapKey("raw", wrapped, JSON.parse("{}"), "AES-KW", hs256, true, ["sign"])); +await attempt("unwrap bad usage name", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", hs256, true, ["fly"])); +await attempt("unwrap hmac without hash", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", { name: "HMAC" }, true, ["sign"])); +await attempt("unwrap unknown key algorithm", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", "TWOFISH", true, ["sign"])); +await attempt("unwrap mismatch", () => crypto.subtle.unwrapKey("raw", wrapped, gcm, "AES-KW", hs256, true, ["sign"])); +await attempt("unwrap missing usage", async () => crypto.subtle.unwrapKey("raw", wrapped, await crypto.subtle.importKey("raw", new Uint8Array(16).fill(1), "AES-KW", true, ["wrapKey"]), "AES-KW", hs256, true, ["sign"])); +await attempt("unwrap tampered", () => crypto.subtle.unwrapKey("raw", tampered, kek, "AES-KW", hs256, true, ["sign"])); +await attempt("unwrap 16 bytes", () => crypto.subtle.unwrapKey("raw", new Uint8Array(16), kek, "AES-KW", hs256, true, ["sign"])); +await attempt("unwrap 28 bytes", () => crypto.subtle.unwrapKey("raw", new Uint8Array(28), kek, "AES-KW", hs256, true, ["sign"])); +await attempt("unwrap bad key usage", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", hs256, true, ["encrypt"])); +await attempt("unwrap empty usages", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", hs256, true, [])); +await attempt("unwrap as pbkdf2", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", "PBKDF2", false, ["deriveBits"])); +await attempt("unwrap as extractable pbkdf2", () => crypto.subtle.unwrapKey("raw", wrapped, kek, "AES-KW", "PBKDF2", true, ["deriveBits"])); +await attempt("unwrap raw bytes as jwk", () => crypto.subtle.unwrapKey("jwk", notJson, kek, "AES-KW", hs256, true, ["sign"])); +const jwkWrapped = await crypto.subtle.wrapKey("jwk", hmac32, kek, "AES-KW"); +await attempt("unwrap jwk with other usages", () => crypto.subtle.unwrapKey("jwk", jwkWrapped, kek, "AES-KW", hs256, true, ["sign", "verify"])); +await attempt("unwrap jwk as AES", () => crypto.subtle.unwrapKey("jwk", jwkWrapped, kek, "AES-KW", "AES-GCM", true, ["encrypt"])); +await attempt("unwrap jwk ok", () => crypto.subtle.unwrapKey("jwk", jwkWrapped, kek, "AES-KW", hs256, false, ["sign"])); diff --git a/tests/corpus/7132-jose-pbes2/main.mjs b/tests/corpus/7132-jose-pbes2/main.mjs new file mode 100644 index 0000000000..1c0f1df135 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/main.mjs @@ -0,0 +1,50 @@ +// The real jose@6.2.12 PBES2 and AES key-wrap paths: EncryptJWT and +// jwtDecrypt from jose's public jose/jwt/encrypt, jose/jwt/decrypt and +// jose/errors entry points, over crypto.subtle importKey, deriveBits, +// wrapKey, unwrapKey, exportKey, encrypt and decrypt. node_modules/jose +// vendors the dist/webapi files those entry points load and their +// declarations, unmodified (MIT, LICENSE.md), with a package.json that +// exports only those entry points. Content keys and IVs are random, so +// tokens are checked by shape and by what they decrypt to. +import { EncryptJWT } from "jose/jwt/encrypt"; +import { jwtDecrypt } from "jose/jwt/decrypt"; +import * as errors from "jose/errors"; + +const encoder = new TextEncoder(); +const password = encoder.encode("correct horse battery staple"); +const kek = new Uint8Array(32); +for (let i = 0; i < kek.length; i++) kek[i] = i * 13 + 5; + +async function roundTrip(label, alg, enc, key, header = {}) { + const token = await new EncryptJWT({ sub: "alice", scope: "read", n: 1 }) + .setProtectedHeader({ alg, enc, ...header }) + .setIssuedAt(1_799_999_000) + .encrypt(key); + const parts = token.split("."); + const { payload, protectedHeader } = await jwtDecrypt(token, key, { + keyManagementAlgorithms: [alg], + currentDate: new Date(1_800_000_000_000), + }); + console.log(label, parts.length, parts[1].length > 0, payload.sub, payload.scope, payload.iat); + console.log(label, protectedHeader.alg, protectedHeader.enc, protectedHeader.p2c ?? "-"); + return token; +} + +const pbes = await roundTrip("pbes2 256", "PBES2-HS256+A128KW", "A256GCM", password); +await roundTrip("pbes2 384", "PBES2-HS384+A192KW", "A128CBC-HS256", password); +await roundTrip("pbes2 512", "PBES2-HS512+A256KW", "A256CBC-HS512", password); +await roundTrip("a128kw", "A128KW", "A128GCM", kek.subarray(0, 16)); +await roundTrip("a256kw", "A256KW", "A256CBC-HS512", kek); + +try { + await jwtDecrypt(pbes, encoder.encode("wrong password"), { keyManagementAlgorithms: ["PBES2-HS256+A128KW"] }); + console.log("wrong password accepted"); +} catch (e) { + console.log("wrong password", e instanceof errors.JWEDecryptionFailed, e.code); +} +try { + await jwtDecrypt(pbes, password); + console.log("unlisted algorithm accepted"); +} catch (e) { + console.log("unlisted algorithm", e instanceof errors.JOSEAlgNotAllowed, e.code); +} diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/LICENSE.md b/tests/corpus/7132-jose-pbes2/node_modules/jose/LICENSE.md new file mode 100644 index 0000000000..d0ec038a85 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Filip Skokan + +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/corpus/7132-jose-pbes2/node_modules/jose/dist/types/jwt/decrypt.d.ts b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/jwt/decrypt.d.ts new file mode 100644 index 0000000000..cdf6a372a1 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/jwt/decrypt.d.ts @@ -0,0 +1,34 @@ +import type * as t from '../types.d.ts'; +/** JWE decryption and JWT Claims Set validation options. */ +export interface JWTDecryptOptions extends t.DecryptOptions, t.JWTClaimVerificationOptions { +} +/** + * Resolves a JWT decryption key. No token components have been authenticated when this function is + * called. + */ +export interface JWTDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a Compact JWE-formatted JWT and validates its Claims Set. + * + * @param key Private key or shared secret to decrypt and verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function jwtDecrypt(jwt: string | Uint8Array, key: t.KeyInput, options?: JWTDecryptOptions): Promise>; +/** + * Decrypts a JWT and validates its claims, returning the dynamically + * {@link t.ResolvedKey.key resolved key}. + * + * @param getKey Function resolving a private key or shared secret to decrypt and verify the JWT + * with. See {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function jwtDecrypt(jwt: string | Uint8Array, getKey: JWTDecryptGetKey, options?: JWTDecryptOptions): Promise & t.ResolvedKey>; +/** + * Accepts a key or key resolver. Use this overload when forwarding either form; the result includes + * `key` only when a resolver was used. + * + * @param key Private key or shared secret, or a function resolving one, to decrypt and verify the + * JWT with. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function jwtDecrypt(jwt: string | Uint8Array, key: t.KeyInput | JWTDecryptGetKey, options?: JWTDecryptOptions): Promise & Partial>; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/jwt/encrypt.d.ts b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/jwt/encrypt.d.ts new file mode 100644 index 0000000000..c39e4c994f --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/jwt/encrypt.d.ts @@ -0,0 +1,57 @@ +import type * as t from '../types.d.ts'; +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const EncryptJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Builds and encrypts Compact JWE-formatted JSON Web Tokens. */ +export declare class EncryptJWT extends EncryptJWT_base { + #private; + /** + * Sets the JWE Protected Header. May only be called once. + * + * @param protectedHeader JWE Protected Header. Must contain an "alg" (JWE Algorithm) and "enc" + * (JWE Encryption Algorithm) properties. + */ + setProtectedHeader(protectedHeader: t.CompactJWEHeaderParameters): this; + /** + * Sets JWE Key Management parameters, such as ECDH-ES "apu" and "apv" or PBES2 "p2c", and adds + * them to the appropriate JOSE Header. Use this instead of the header setters for algorithm + * inputs. May only be called once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this; + /** + * Sets the content encryption key. By default, a suitable random key is generated for the JWE + * "enc" (Encryption Algorithm). May only be called once. + * + * @deprecated For testing and vector validation only; allow random generation in production. + */ + setContentEncryptionKey(cek: Uint8Array): this; + /** + * Sets the Initialization Vector for content encryption. By default, a suitable random IV is + * generated for the JWE "enc" (Encryption Algorithm). May only be called once. + * + * @deprecated For testing and vector validation only; allow random generation in production. + */ + setInitializationVector(iv: Uint8Array): this; + /** + * Replicates the "iss" (Issuer) Claim in the JWE Protected Header, exposing it without + * decryption. + */ + replicateIssuerAsHeader(): this; + /** + * Replicates the "sub" (Subject) Claim in the JWE Protected Header, exposing it without + * decryption. + */ + replicateSubjectAsHeader(): this; + /** + * Replicates the "aud" (Audience) Claim in the JWE Protected Header, exposing it without + * decryption. + */ + replicateAudienceAsHeader(): this; + /** + * Encrypts and returns the JWT. + * + * @param key Public key or shared secret to encrypt the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise; +} +export {}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/types.d.ts b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/types.d.ts new file mode 100644 index 0000000000..9815c018d5 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/types.d.ts @@ -0,0 +1,736 @@ +/** Supported JWS "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWSAlgorithm = + | 'HS256' + | 'HS384' + | 'HS512' + | 'RS256' + | 'RS384' + | 'RS512' + | 'PS256' + | 'PS384' + | 'PS512' + | 'ES256' + | 'ES384' + | 'ES512' + | 'EdDSA' + | 'Ed25519' + | 'ML-DSA-44' + | 'ML-DSA-65' + | 'ML-DSA-87' + | (string & {}) + +/** Supported JWE "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWEKeyManagementAlgorithm = + | 'dir' + | 'A128KW' + | 'A192KW' + | 'A256KW' + | 'A128GCMKW' + | 'A192GCMKW' + | 'A256GCMKW' + | 'ECDH-ES' + | 'ECDH-ES+A128KW' + | 'ECDH-ES+A192KW' + | 'ECDH-ES+A256KW' + | 'RSA-OAEP' + | 'RSA-OAEP-256' + | 'RSA-OAEP-384' + | 'RSA-OAEP-512' + | 'PBES2-HS256+A128KW' + | 'PBES2-HS384+A192KW' + | 'PBES2-HS512+A256KW' + | (string & {}) + +/** + * Supported JWE "enc" (Encryption Algorithm) Header Parameter values. Availability depends on the + * runtime. + */ +export type JWEContentEncryptionAlgorithm = + | 'A128CBC-HS256' + | 'A192CBC-HS384' + | 'A256CBC-HS512' + | 'A128GCM' + | 'A192GCM' + | 'A256GCM' + | (string & {}) + +/** JWK "kty" (Key Type) Parameter values supported by this module. */ +export type JWKKeyType = 'EC' | 'RSA' | 'OKP' | 'AKP' | 'oct' | (string & {}) + +/** + * Generic JSON Web Key Parameters. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. + */ +export type JWKParameters = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string +} + +/** Convenience interface for public OKP JSON Web Keys. */ +export interface JWK_OKP_Public extends JWKParameters { + /** Key pair subtype. */ + crv: string + /** Public key. */ + x: string +} + +/** Convenience interface for private OKP JSON Web Keys. */ +export interface JWK_OKP_Private extends JWK_OKP_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public AKP JSON Web Keys. */ +export interface JWK_AKP_Public extends JWKParameters { + /** JWK "alg" (Algorithm) Parameter */ + alg: string + + /** AKP JWK "pub" (The Public key) Parameter */ + pub: string +} + +/** Convenience interface for private AKP JSON Web Keys. */ +export interface JWK_AKP_Private extends JWK_AKP_Public { + /** AKP JWK "priv" (The Private Key) Parameter */ + priv: string +} + +/** Convenience interface for public EC JSON Web Keys. */ +export interface JWK_EC_Public extends JWKParameters { + /** Curve. */ + crv: string + /** Public key X coordinate. */ + x: string + /** Public key Y coordinate. */ + y: string +} + +/** Convenience interface for private EC JSON Web Keys. */ +export interface JWK_EC_Private extends JWK_EC_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public RSA JSON Web Keys. */ +export interface JWK_RSA_Public extends JWKParameters { + /** Public exponent. */ + e: string + /** Modulus. */ + n: string +} + +/** Convenience interface for private RSA JSON Web Keys. */ +export interface JWK_RSA_Private extends JWK_RSA_Public { + /** Private exponent. */ + d: string + /** First factor CRT exponent. */ + dp: string + /** Second factor CRT exponent. */ + dq: string + /** First prime factor. */ + p: string + /** Second prime factor. */ + q: string + /** First CRT coefficient. */ + qi: string +} + +/** Convenience interface for "oct" JSON Web Keys. */ +export interface JWK_oct extends JWKParameters { + /** Symmetric key value. */ + k: string +} + +/** + * JSON Web Key ({@link https://www.rfc-editor.org/info/rfc7517/ JWK}). "RSA", "EC", "OKP", "AKP", + * and "oct" key types are supported. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. It spells out the + * > {@link JWKParameters} members rather than intersecting them so that every JWK member is documented + * > in one place. + */ +export type JWK = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string + /** EC curve or OKP key pair subtype. */ + crv?: string + /** Private RSA exponent, EC key, or OKP key. */ + d?: string + /** RSA first factor CRT exponent. */ + dp?: string + /** RSA second factor CRT exponent. */ + dq?: string + /** RSA public exponent. */ + e?: string + /** Symmetric key value. */ + k?: string + /** RSA modulus. */ + n?: string + /** RSA first prime factor. */ + p?: string + /** RSA second prime factor. */ + q?: string + /** RSA first CRT coefficient. */ + qi?: string + /** EC public key X coordinate or OKP public key. */ + x?: string + /** EC public key Y coordinate. */ + y?: string + + /** AKP JWK "pub" (Public Key) Parameter */ + pub?: string + + /** AKP JWK "priv" (Private key) Parameter */ + priv?: string + /** Additional RSA prime factors. */ + oth?: Array<{ + /** Factor CRT exponent. */ + d?: string + /** Prime factor. */ + r?: string + /** Factor CRT coefficient. */ + t?: string + }> +} + +/** Discriminated union of supported JSON Web Key shapes, narrowed by the "kty" (Key Type) Parameter. */ +// Intersect "kty" into each member separately: typedoc omits the parentheses in `X & (A | B)`. +export type AnyJWK = + | (JWK_EC_Private & { kty: 'EC' }) + | (JWK_EC_Public & { kty: 'EC' }) + | (JWK_RSA_Private & { kty: 'RSA' }) + | (JWK_RSA_Public & { kty: 'RSA' }) + | (JWK_OKP_Private & { kty: 'OKP' }) + | (JWK_OKP_Public & { kty: 'OKP' }) + | (JWK_AKP_Private & { kty: 'AKP' }) + | (JWK_AKP_Public & { kty: 'AKP' }) + | (JWK_oct & { kty: 'oct' }) + +/** Key or secret input accepted by all sign, verify, encrypt, and decrypt operations. */ +export type KeyInput = CryptoKey | KeyObject | JWK | Uint8Array + +/** Dynamic key resolver for consuming operations. */ +export interface GetKeyFunction { + /** + * Resolves a key for an unverified token. Throw if no suitable key can be resolved. + * + * @param token The consumed JWE or JWS token; none of its components have been verified. + */ + (protectedHeader: IProtectedHeader, token: IToken): Promise | KeyTypes +} + +/** Flattened JWS verification input. */ +export interface FlattenedJWSInput { + /** JWS Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + header?: JWSHeaderParameters + + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** Base64url-encoded UTF-8 JWS Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded signature or MAC. */ + signature: string +} + +/** General JWS verification input. */ +export interface GeneralJWSInput { + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** + * The "signatures" member value MUST be an array of JSON objects. Each object represents a + * signature or MAC over the JWS Payload and the JWS Protected Header. + */ + signatures: Omit[] +} + +/** + * Flattened JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface FlattenedJWS extends Partial { + payload: string + signature: string +} + +/** + * General JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface GeneralJWS { + payload: string + signatures: Omit[] +} + +/** Header Parameters common to JWE and JWS. */ +export interface JoseHeaderParameters { + /** "kid" (Key ID) Header Parameter */ + kid?: string + + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + + /** X.509 certificate chain. */ + x5c?: string[] + + /** X.509 certificate URL. */ + x5u?: string + + /** JWK Set URL. */ + jku?: string + + /** Public JWK only; private and symmetric key parameters are not permitted. */ + jwk?: Omit + + /** "typ" (Type) Header Parameter */ + typ?: string + + /** Content type. */ + cty?: string +} + +/** Recognized JWS Header Parameters; additional members may also be present. */ +export interface JWSHeaderParameters extends JoseHeaderParameters { + /** JWS "alg" (Algorithm) Header Parameter */ + alg?: JWSAlgorithm + + /** + * Controls payload encoding and the JWS signing input as defined by + * {@link https://www.rfc-editor.org/info/rfc7797/ RFC7797}. Set to `false` and list `b64` in + * `crit` to use an unencoded payload. + */ + b64?: boolean + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** Any other JWS Header member. */ + [propName: string]: unknown +} + +/** Recognized JWE Key Management-related Header Parameters. */ +export interface JWEKeyManagementHeaderParameters { + /** ECDH-ES Agreement PartyUInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apu?: Uint8Array + + /** ECDH-ES Agreement PartyVInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apv?: Uint8Array + + /** PBES2 PBKDF2 iteration count, added to the JOSE header. */ + p2c?: number + + /** @deprecated For testing and vector validation only. */ + p2s?: Uint8Array + /** @deprecated For testing and vector validation only. */ + iv?: Uint8Array + /** @deprecated For testing and vector validation only. */ + epk?: CryptoKey | KeyObject +} + +/** Flattened JWE JSON Serialization token. */ +export interface FlattenedJWE { + /** + * Base64url-encoded additional authenticated data; integrity protected but not encrypted. Omit + * when empty. + */ + aad?: string + + /** Base64url-encoded ciphertext. */ + ciphertext: string + + /** Base64url-encoded encrypted key. Omit when empty. */ + encrypted_key?: string + + /** + * JWE Per-Recipient Unprotected Header as a JSON object. Not integrity protected; omit when + * empty. + */ + header?: JWEHeaderParameters + + /** Base64url-encoded initialization vector. Omit when empty. */ + iv?: string + + /** Base64url-encoded UTF-8 JWE Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded authentication tag. Omit when empty. */ + tag?: string + + /** JWE Shared Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + unprotected?: JWEHeaderParameters +} + +/** General JWE JSON Serialization token. */ +export interface GeneralJWE extends Omit { + recipients: Pick[] +} + +/** Recognized JWE Header Parameters; additional members may also be present. */ +export interface JWEHeaderParameters extends JoseHeaderParameters { + /** JWE "alg" (Algorithm) Header Parameter */ + alg?: JWEKeyManagementAlgorithm + + /** JWE "enc" (Encryption Algorithm) Header Parameter */ + enc?: JWEContentEncryptionAlgorithm + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** + * JWE compression algorithm. Only `"DEF"` (DEFLATE) is supported, requiring the runtime's + * `CompressionStream` / `DecompressionStream` APIs. + */ + zip?: 'DEF' | (string & {}) + + /** Any other JWE Header member. */ + [propName: string]: unknown +} + +/** Shared "crit" option for signing, verification, encryption, and decryption. */ +export interface CritOption { + /** + * Recognized "crit" (Critical) Header Parameter names. Set each value to `true` to require + * integrity protection, or `false` when protection is optional. The JWS `b64` extension is always + * recognized and processed. + * + * > Warning: Other extensions are only checked for syntax and optional integrity protection. Their presence + * > is not required by this option. You must check their presence and process them according to the + * > profile's validation steps after the operation succeeds. + */ + crit?: { + [propName: string]: boolean + } +} + +/** JWE Decryption options. */ +export interface DecryptOptions extends CritOption { + /** + * Accepted JWE "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret except PBES2, which must be explicitly allowed. + */ + keyManagementAlgorithms?: JWEKeyManagementAlgorithm[] + + /** + * Accepted JWE "enc" (Encryption Algorithm) Header Parameter values. Defaults to all algorithms + * applicable to the key or secret. + */ + contentEncryptionAlgorithms?: JWEContentEncryptionAlgorithm[] + + /** + * Maximum "p2c" (PBES2 Count) Header Parameter value, limiting PBKDF2 iterations and their + * computational expense. Defaults to 10000; must be a positive safe integer or `Infinity` to + * disable the limit. + */ + maxPBES2Count?: number + + /** + * Maximum decompressed plaintext size in bytes. Defaults to 250000; `0` rejects compressed JWEs, + * and `Infinity` disables the limit. Other values must be positive safe integers. + */ + maxDecompressedLength?: number +} + +/** JWE Encryption options. */ +export interface EncryptOptions extends CritOption {} + +/** JWT Claims Set verification options. */ +export interface JWTClaimVerificationOptions { + /** Expected JWT "aud" (Audience) Claim value(s). Requires the claim to be present. */ + audience?: string | string[] + + /** + * Clock skew tolerance in seconds or a duration string (e.g. "5 seconds"). Applies to the "nbf" + * (Not Before) and "exp" (Expiration Time) claims, and to "iat" (Issued At) when + * {@link maxTokenAge} is set. + */ + clockTolerance?: string | number + + /** Expected JWT "iss" (Issuer) Claim value(s). Requires the claim to be present. */ + issuer?: string | string[] + + /** + * Maximum time since the JWT "iat" (Issued At) Claim, in seconds or a duration string (e.g. "2 + * hours"). Requires the claim to be present. + */ + maxTokenAge?: string | number + + /** Expected JWT "sub" (Subject) Claim value. Requires the claim to be present. */ + subject?: string + + /** Expected JWT "typ" (Type) Header Parameter value. Requires the parameter to be present. */ + typ?: string + + /** Date for NumericDate comparisons. Defaults to `new Date()`. */ + currentDate?: Date + + /** + * Additional claim names required in the JWT Claims Set. The {@link issuer}, {@link audience}, + * {@link subject}, and {@link maxTokenAge} options independently require "iss", "aud", "sub", and + * "iat", respectively. + */ + requiredClaims?: string[] +} + +/** JWS Verification options. */ +export interface VerifyOptions extends CritOption { + /** + * Accepted JWS "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret. Unsecured JWTs (`alg: "none"`) are never accepted. + */ + algorithms?: JWSAlgorithm[] +} + +/** JWS Signing options. */ +export interface SignOptions extends CritOption {} + +/** Recognized JWT Claims Set members; additional members may also be present. */ +export interface JWTPayload { + /** JWT Issuer */ + iss?: string + + /** JWT Subject */ + sub?: string + + /** JWT Audience */ + aud?: string | string[] + + /** JWT ID */ + jti?: string + + /** Not valid before this Unix timestamp in seconds. */ + nbf?: number + + /** Expiration Unix timestamp in seconds. */ + exp?: number + + /** Issued-at Unix timestamp in seconds. */ + iat?: number + + /** Any other JWT Claim Set member. */ + [propName: string]: unknown +} + +/** Flattened JWE JSON Serialization decryption result. */ +export interface FlattenedDecryptResult { + /** JWE Additional Authenticated Data, integrity protected but not encrypted. */ + additionalAuthenticatedData?: Uint8Array + + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader?: JWEHeaderParameters + + /** JWE Shared Unprotected Header. Not integrity protected. */ + sharedUnprotectedHeader?: JWEHeaderParameters + + /** JWE Per-Recipient Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWEHeaderParameters +} + +/** General JWE JSON Serialization decryption result. */ +export interface GeneralDecryptResult extends FlattenedDecryptResult {} + +/** Compact JWE decryption result. */ +export interface CompactDecryptResult { + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Flattened JWS JSON Serialization verification result. */ +export interface FlattenedVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader?: JWSHeaderParameters + + /** JWS Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWSHeaderParameters +} + +/** General JWS JSON Serialization verification result. */ +export interface GeneralVerifyResult extends FlattenedVerifyResult {} + +/** Compact JWS verification result. */ +export interface CompactVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader: CompactJWSHeaderParameters +} + +/** Signed JSON Web Token (JWT) verification result. */ +export interface JWTVerifyResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWS Protected Header. */ + protectedHeader: JWTHeaderParameters +} + +/** Encrypted JSON Web Token (JWT) decryption result. */ +export interface JWTDecryptResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Key resolver result metadata. */ +export interface ResolvedKey { + /** The resolved key, present only when a key resolver is used. */ + key: KeyType +} + +/** Recognized Compact JWS Header Parameters; additional members may also be present. */ +export interface CompactJWSHeaderParameters extends JWSHeaderParameters { + alg: JWSAlgorithm +} + +/** Recognized signed JWT Header Parameters; additional members may also be present. */ +export interface JWTHeaderParameters extends CompactJWSHeaderParameters { + b64?: boolean +} + +/** Recognized Compact JWE Header Parameters; additional members may also be present. */ +export interface CompactJWEHeaderParameters extends JWEHeaderParameters { + alg: JWEKeyManagementAlgorithm + enc: JWEContentEncryptionAlgorithm +} + +/** JSON Web Key Set. */ +export interface JSONWebKeySet { + keys: JWK[] +} + +/** Node.js {@link !KeyObject} representation accepted as key input. */ +export interface KeyObject { + type: string +} + +/** The runtime's Web Crypto {@link !CryptoKey} representation accepted as key input. */ +export type CryptoKey = typeof globalThis extends { + crypto: { subtle: { generateKey(...args: any[]): Promise } } +} + ? Extract + : CryptoKeyStructuralFallback + +/** Structural fallback used when the host {@link !CryptoKey} type cannot be inferred. */ +export interface CryptoKeyStructuralFallback { + readonly algorithm: { name: string } + readonly extractable: boolean + readonly type: string + readonly usages: string[] +} + +/** Shared fluent API for JWT-producing classes. */ +export interface ProduceJWT { + /** + * Set the "iss" (Issuer) Claim. + * + * @param issuer "Issuer" Claim value to set on the JWT Claims Set. + */ + setIssuer(issuer: string): this + + /** + * Set the "sub" (Subject) Claim. + * + * @param subject "sub" (Subject) Claim value to set on the JWT Claims Set. + */ + setSubject(subject: string): this + + /** + * Set the "aud" (Audience) Claim. + * + * @param audience "aud" (Audience) Claim value to set on the JWT Claims Set. + */ + setAudience(audience: string | string[]): this + + /** + * Set the "jti" (JWT ID) Claim. + * + * @param jwtId "jti" (JWT ID) Claim value to set on the JWT Claims Set. + */ + setJti(jwtId: string): this + + /** + * Set the "nbf" (Not Before) Claim. Numbers are Unix timestamps in seconds; Dates are converted + * to seconds. Strings are relative to now, using seconds, minutes, hours, days, weeks, or years + * (365.25 days; no months). Prefix `-` or suffix `ago` subtracts the duration. + * + * @param input "nbf" (Not Before) Claim value as a timestamp, Date, or relative duration. + */ + setNotBefore(input: number | string | Date): this + + /** + * Set the "exp" (Expiration Time) Claim. Accepts a Unix timestamp in seconds, a Date, or a + * duration relative to now using the same formats as {@link setNotBefore}. + * + * @param input "exp" (Expiration Time) Claim value as a timestamp, Date, or relative duration. + */ + setExpirationTime(input: number | string | Date): this + + /** + * Set the "iat" (Issued At) Claim. Defaults to the current Unix timestamp in seconds. Accepts a + * Unix timestamp in seconds, a Date, or a duration relative to now using the same formats as + * {@link setNotBefore}. + * + * @param input "iat" (Issued At) Claim value as a timestamp, Date, or relative duration. + */ + setIssuedAt(input?: number | string | Date): this +} diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/util/errors.d.ts b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/util/errors.d.ts new file mode 100644 index 0000000000..930a29b9f6 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/types/util/errors.d.ts @@ -0,0 +1,182 @@ +import type * as t from '../types.d.ts'; +/** Stable error codes used by this module. */ +export type JOSEErrorCode = 'ERR_JOSE_ALG_NOT_ALLOWED' | 'ERR_JOSE_GENERIC' | 'ERR_JOSE_NOT_SUPPORTED' | 'ERR_JWE_DECRYPTION_FAILED' | 'ERR_JWE_INVALID' | 'ERR_JWK_INVALID' | 'ERR_JWKS_INVALID' | 'ERR_JWKS_MULTIPLE_MATCHING_KEYS' | 'ERR_JWKS_NO_MATCHING_KEY' | 'ERR_JWKS_TIMEOUT' | 'ERR_JWS_INVALID' | 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED' | 'ERR_JWT_CLAIM_VALIDATION_FAILED' | 'ERR_JWT_EXPIRED' | 'ERR_JWT_INVALID'; +/** Shared details of JWT claim or header validation failures. */ +export interface JWTClaimValidationFailure { + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** The parsed JWT Claims Set; validation of other claims may be incomplete. */ + payload: t.JWTPayload; +} +/** Reason codes produced by JWT Claims Set validation. */ +export type JWTClaimValidationReason = 'check_failed' | 'invalid' | 'mismatch' | 'missing' | 'unspecified' | (string & {}); +/** Base class for JOSE errors. */ +export declare class JOSEError extends Error { + /** Stable code identifying the error class. */ + static code: JOSEErrorCode | (string & {}); + /** Stable code identifying the error class. Use {@link AnyJOSEError} to narrow subclasses by code. */ + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** + * Thrown when JWT claim or header validation fails. Expiration is reported separately as + * {@link JWTExpired}. + */ +export declare class JWTClaimValidationFailed extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when a JWT has expired or exceeds the configured maximum token age. */ +export declare class JWTExpired extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when an algorithm is disallowed by configuration. */ +export declare class JOSEAlgNotAllowed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a feature or algorithm is unsupported. */ +export declare class JOSENotSupported extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when JWE ciphertext decryption or authentication fails. */ +export declare class JWEDecryptionFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when a JWE is invalid. */ +export declare class JWEInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWS is invalid. */ +export declare class JWSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWT is invalid. */ +export declare class JWTInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWK is invalid. */ +export declare class JWKInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWKS is invalid. */ +export declare class JWKSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when no keys match in a JWKS. */ +export declare class JWKSNoMatchingKey extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when multiple keys match in a JWKS. */ +export declare class JWKSMultipleMatchingKeys extends JOSEError { + /** + * Yields public keys matching the JWS Header so verification can be attempted with each. See the + * {@link jwks/remote.createRemoteJWKSet createRemoteJWKSet} and + * {@link jwks/local.createLocalJWKSet createLocalJWKSet} examples. Empty on manually constructed + * errors. + */ + [Symbol.asyncIterator]: () => AsyncIterableIterator; + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when fetching a remote JWKS times out. */ +export declare class JWKSTimeout extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when JWS signature verification fails. */ +export declare class JWSSignatureVerificationFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Errors thrown during JWT Claims Set validation. */ +export type JWTClaimValidationError = JWTClaimValidationFailed | JWTExpired; +/** + * Discriminated union of specific {@link JOSEError} subclasses, narrowed by `code`. Excludes base + * {@link JOSEError} instances; use `instanceof JOSEError` to catch all module errors. + */ +export type AnyJOSEError = (JOSEAlgNotAllowed & { + code: 'ERR_JOSE_ALG_NOT_ALLOWED'; +}) | (JOSENotSupported & { + code: 'ERR_JOSE_NOT_SUPPORTED'; +}) | (JWEDecryptionFailed & { + code: 'ERR_JWE_DECRYPTION_FAILED'; +}) | (JWEInvalid & { + code: 'ERR_JWE_INVALID'; +}) | (JWKInvalid & { + code: 'ERR_JWK_INVALID'; +}) | (JWKSInvalid & { + code: 'ERR_JWKS_INVALID'; +}) | (JWKSMultipleMatchingKeys & { + code: 'ERR_JWKS_MULTIPLE_MATCHING_KEYS'; +}) | (JWKSNoMatchingKey & { + code: 'ERR_JWKS_NO_MATCHING_KEY'; +}) | (JWKSTimeout & { + code: 'ERR_JWKS_TIMEOUT'; +}) | (JWSInvalid & { + code: 'ERR_JWS_INVALID'; +}) | (JWSSignatureVerificationFailed & { + code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED'; +}) | (JWTClaimValidationFailed & { + code: 'ERR_JWT_CLAIM_VALIDATION_FAILED'; +}) | (JWTExpired & { + code: 'ERR_JWT_EXPIRED'; +}) | (JWTInvalid & { + code: 'ERR_JWT_INVALID'; +}); diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/jwt/decrypt.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/jwt/decrypt.js new file mode 100644 index 0000000000..cfbe4730cc --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/jwt/decrypt.js @@ -0,0 +1,13 @@ +import { prepareDecrypt, decryptCompact } from "../lib/jwe_decrypt.js"; +import { validateClaimsSet } from "../lib/jwt_claims_set.js"; +import { JWTClaimValidationFailed } from "../util/errors.js"; +async function jwtDecrypt(jwt, key, options) { + const { plaintext, ...result } = await decryptCompact(jwt, prepareDecrypt(options), key), { protectedHeader } = result, payload = validateClaimsSet(protectedHeader, plaintext, options); + for (const claim of ["iss", "sub", "aud"]) + if (protectedHeader[claim] !== void 0 && (claim === "aud" ? JSON.stringify(protectedHeader.aud) !== JSON.stringify(payload.aud) : protectedHeader[claim] !== payload[claim])) + throw new JWTClaimValidationFailed(`replicated "${claim}" claim header parameter mismatch`, payload, claim, "mismatch"); + return { payload, ...result }; +} +export { + jwtDecrypt +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/jwt/encrypt.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/jwt/encrypt.js new file mode 100644 index 0000000000..c269088570 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/jwt/encrypt.js @@ -0,0 +1,45 @@ +import { compactJWE, createJWE } from "../lib/jwe_encrypt.js"; +import { JWTClaimsBuilder, jwtClaim, jwtData } from "../lib/jwt_claims_set.js"; +import { assertNotSet } from "../lib/validate.js"; +const EncryptJWT_base = JWTClaimsBuilder; +class EncryptJWT extends EncryptJWT_base { + #input = [void 0]; + #replicateIssuerAsHeader; + #replicateSubjectAsHeader; + #replicateAudienceAsHeader; + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.#input[7], "setKeyManagementParameters"), this.#input[7] = parameters, this; + } + setContentEncryptionKey(cek) { + return assertNotSet(this.#input[5], "setContentEncryptionKey"), this.#input[5] = cek, this; + } + setInitializationVector(iv) { + return assertNotSet(this.#input[6], "setInitializationVector"), this.#input[6] = iv, this; + } + replicateIssuerAsHeader() { + return this.#replicateIssuerAsHeader = !0, this; + } + replicateSubjectAsHeader() { + return this.#replicateSubjectAsHeader = !0, this; + } + replicateAudienceAsHeader() { + return this.#replicateAudienceAsHeader = !0, this; + } + async encrypt(key, options) { + const plaintext = jwtData(this); + this.#input[1] && (this.#replicateIssuerAsHeader || this.#replicateSubjectAsHeader || this.#replicateAudienceAsHeader) && (this.#input[1] = { + ...this.#input[1], + iss: this.#replicateIssuerAsHeader ? jwtClaim(this, "iss") : void 0, + sub: this.#replicateSubjectAsHeader ? jwtClaim(this, "sub") : void 0, + aud: this.#replicateAudienceAsHeader ? jwtClaim(this, "aud") : void 0 + }); + const input = [...this.#input]; + return input[0] = plaintext, compactJWE(await createJWE(input, key, options)); + } +} +export { + EncryptJWT +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/buffer_utils.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/buffer_utils.js new file mode 100644 index 0000000000..b3cad85a93 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/buffer_utils.js @@ -0,0 +1,75 @@ +const encoder = new TextEncoder(), decoder = new TextDecoder(), strictDecoder = new TextDecoder("utf-8", { fatal: !0 }), MAX_INT32 = 2 ** 32; +function concat(...buffers) { + const size = buffers.reduce((acc, { length }) => acc + length, 0), buf = new Uint8Array(size); + let i = 0; + for (const buffer of buffers) + buf.set(buffer, i), i += buffer.length; + return buf; +} +function writeUInt32BE(buf, value, offset) { + if (value < 0 || value >= MAX_INT32) + throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`); + buf.set([value >>> 24, value >>> 16, value >>> 8, value & 255], offset); +} +function uint64be(value) { + const high = Math.floor(value / MAX_INT32), low = value % MAX_INT32, buf = new Uint8Array(8); + return writeUInt32BE(buf, high, 0), writeUInt32BE(buf, low, 4), buf; +} +function uint32be(value) { + const buf = new Uint8Array(4); + return writeUInt32BE(buf, value), buf; +} +const NON_ASCII = /[^\x00-\x7f]/; +function encode(string) { + if (typeof string == "string" && string.length >= 128) { + if (NON_ASCII.test(string)) + throw new TypeError("non-ASCII string encountered in encode()"); + return encoder.encode(string); + } + const bytes = new Uint8Array(string.length); + for (let i = 0; i < string.length; i++) { + const code = string.charCodeAt(i); + if (code > 127) + throw new TypeError("non-ASCII string encountered in encode()"); + bytes[i] = code; + } + return bytes; +} +function encodeBase64(input, url = !1) { + if (Uint8Array.prototype.toBase64) + return input.toBase64({ alphabet: url ? "base64url" : "base64", omitPadding: url }); + const CHUNK_SIZE = 32768, arr = []; + for (let i = 0; i < input.length; i += CHUNK_SIZE) + arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); + const encoded = btoa(arr.join("")); + return url ? encoded.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") : encoded; +} +function decodeBase64(encoded, url = !1) { + if (Uint8Array.fromBase64) + return Uint8Array.fromBase64(encoded, { alphabet: url ? "base64url" : "base64" }); + if (url) { + if (encoded.includes("+") || encoded.includes("/")) + throw new TypeError("Invalid base64url"); + encoded = encoded.replace(/-/g, "+").replace(/_/g, "/"); + } + const binary = atob(encoded), bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) + bytes[i] = binary.charCodeAt(i); + return bytes; +} +async function digest(algorithm, data) { + const subtleDigest = `SHA-${algorithm.slice(-3)}`; + return new Uint8Array(await crypto.subtle.digest(subtleDigest, data)); +} +export { + concat, + decodeBase64, + decoder, + digest, + encode, + encodeBase64, + encoder, + strictDecoder, + uint32be, + uint64be +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/content_encryption.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/content_encryption.js new file mode 100644 index 0000000000..1ea355e91c --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/content_encryption.js @@ -0,0 +1,90 @@ +import { concat, uint64be } from "./buffer_utils.js"; +import { rawKey, invalidKeyInput, isCryptoKey } from "./key.js"; +import { JWEDecryptionFailed, JWEInvalid } from "../util/errors.js"; +const generateCek = (enc) => crypto.getRandomValues(new Uint8Array(enc.cekBits >> 3)); +function checkCekLength(cek, expected) { + const actual = cek.byteLength << 3; + if (actual !== expected) + throw new JWEInvalid(`Invalid Content Encryption Key length. Expected ${expected} bits, got ${actual} bits`); +} +const generateIv = (enc) => crypto.getRandomValues(new Uint8Array(enc.ivBits >> 3)); +function checkIvLength(enc, iv) { + if (iv.length << 3 !== enc.ivBits) + throw new JWEInvalid("Invalid Initialization Vector length"); +} +async function cbcKeySetup(enc, cek, usage) { + if (!(cek instanceof Uint8Array)) + throw new TypeError(invalidKeyInput(cek, "Uint8Array")); + const keySize = enc.cekBits >> 1, encKey = await crypto.subtle.importKey("raw", cek.subarray(keySize >> 3), "AES-CBC", !1, [usage]), macKey = await crypto.subtle.importKey("raw", cek.subarray(0, keySize >> 3), { + hash: `SHA-${keySize << 1}`, + name: "HMAC" + }, !1, ["sign"]); + return [encKey, macKey, keySize]; +} +async function cbcHmacTag(macKey, macData, keySize) { + return new Uint8Array((await crypto.subtle.sign("HMAC", macKey, macData)).slice(0, keySize >> 3)); +} +async function cbcEncrypt(enc, plaintext, cek, iv, aad) { + const [encKey, macKey, keySize] = await cbcKeySetup(enc, cek, "encrypt"), ciphertext = new Uint8Array(await crypto.subtle.encrypt({ + iv, + name: "AES-CBC" + }, encKey, plaintext)), macData = concat(aad, iv, ciphertext, uint64be(aad.length * 8)), tag = await cbcHmacTag(macKey, macData, keySize); + return { ciphertext, tag, iv }; +} +async function timingSafeEqual(a, b) { + const algorithm = { name: "HMAC", hash: "SHA-256" }, key = await crypto.subtle.generateKey(algorithm, !1, ["sign", "verify"]), aHmac = await crypto.subtle.sign(algorithm, key, a); + return crypto.subtle.verify(algorithm, key, aHmac, b); +} +async function cbcDecrypt(enc, cek, ciphertext, iv, tag, aad) { + const [encKey, macKey, keySize] = await cbcKeySetup(enc, cek, "decrypt"), macData = concat(aad, iv, ciphertext, uint64be(aad.length * 8)), expectedTag = await cbcHmacTag(macKey, macData, keySize); + try { + if (await timingSafeEqual(tag, expectedTag)) + return new Uint8Array(await crypto.subtle.decrypt({ iv, name: "AES-CBC" }, encKey, ciphertext)); + } catch { + } + throw new JWEDecryptionFailed(); +} +async function encrypt(enc, plaintext, cek, iv, aad) { + if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) + throw new TypeError(invalidKeyInput(cek, "CryptoKey", "KeyObject", "Uint8Array", "JSON Web Key")); + if (iv ? checkIvLength(enc, iv) : iv = generateIv(enc), cek instanceof Uint8Array && checkCekLength(cek, enc.cekBits), enc.cbc) + return cbcEncrypt(enc, plaintext, cek, iv, aad); + const encKey = await rawKey(cek, enc.subtle, "encrypt"), encrypted = new Uint8Array(await crypto.subtle.encrypt({ + additionalData: aad, + iv, + name: "AES-GCM", + tagLength: 128 + }, encKey, plaintext)); + return { ciphertext: encrypted.subarray(0, -16), tag: encrypted.subarray(-16), iv }; +} +async function decrypt(enc, cek, ciphertext, iv, tag, aad) { + if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) + throw new TypeError(invalidKeyInput(cek, "CryptoKey", "KeyObject", "Uint8Array", "JSON Web Key")); + if (!iv) + throw new JWEInvalid("JWE Initialization Vector missing"); + if (!tag) + throw new JWEInvalid("JWE Authentication Tag missing"); + if (!enc.cbc && tag.length !== 16) + throw new JWEInvalid("Invalid Authentication Tag length"); + if (checkIvLength(enc, iv), cek instanceof Uint8Array && checkCekLength(cek, enc.cekBits), enc.cbc) + return cbcDecrypt(enc, cek, ciphertext, iv, tag, aad); + const encKey = await rawKey(cek, enc.subtle, "decrypt"); + try { + return new Uint8Array(await crypto.subtle.decrypt({ + additionalData: aad, + iv, + name: "AES-GCM", + tagLength: 128 + }, encKey, concat(ciphertext, tag))); + } catch { + throw new JWEDecryptionFailed(); + } +} +export { + checkCekLength, + checkIvLength, + decrypt, + encrypt, + generateCek, + generateIv +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/deflate.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/deflate.js new file mode 100644 index 0000000000..27ffba278a --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/deflate.js @@ -0,0 +1,40 @@ +import { JOSENotSupported, JWEInvalid } from "../util/errors.js"; +import { concat } from "./buffer_utils.js"; +function validateZip(joseHeader, protectedHeader) { + if (joseHeader.zip !== void 0 && joseHeader.zip !== "DEF") + throw new JOSENotSupported('Unsupported JWE "zip" (Compression Algorithm) Header Parameter value.'); + if (joseHeader.zip !== void 0 && !protectedHeader?.zip) + throw new JWEInvalid('JWE "zip" (Compression Algorithm) Header Parameter MUST be in a protected header.'); +} +function supported(name) { + if (typeof globalThis[name] > "u") + throw new JOSENotSupported(`JWE "zip" (Compression Algorithm) Header Parameter requires the ${name} API.`); +} +async function transform(stream, input, maxLength = 1 / 0) { + const writer = stream.writable.getWriter(); + writer.write(input).catch(() => { + }), writer.close().catch(() => { + }); + const chunks = []; + let length = 0; + const reader = stream.readable.getReader(); + for (; ; ) { + const { value, done } = await reader.read(); + if (done) + break; + if (chunks.push(value), length += value.byteLength, maxLength !== 1 / 0 && length > maxLength) + throw new JWEInvalid("Decompressed plaintext exceeded the configured limit"); + } + return concat(...chunks); +} +async function compress(input) { + return supported("CompressionStream"), transform(new CompressionStream("deflate-raw"), input); +} +async function decompress(input, maxLength) { + return supported("DecompressionStream"), transform(new DecompressionStream("deflate-raw"), input, maxLength); +} +export { + compress, + decompress, + validateZip +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_algorithms.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_algorithms.js new file mode 100644 index 0000000000..a87af4eb5e --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_algorithms.js @@ -0,0 +1,116 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { table } from "./key_descriptor.js"; +const wrap = [ + ["encrypt", "wrapKey"], + ["decrypt", "unwrapKey"] +], derive = [[], ["deriveBits"]], none = [[], []]; +function rsaes(bits) { + return { + kty: ["RSA"], + mode: "key-encryption", + subtle: { name: "RSA-OAEP", hash: `SHA-${bits}` }, + usages: wrap, + ops: ["wrapKey", "unwrapKey"] + }; +} +function ecdh(mode) { + return { + kty: ["EC", "OKP"], + mode, + subtle: { name: "ECDH" }, + resolve: ({ kty, crv, asymmetricKeyType }) => { + if (crv === "X25519" || asymmetricKeyType === "x25519") + return { name: "X25519" }; + if (kty === "OKP") + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + return { name: "ECDH", namedCurve: crv }; + }, + usages: derive, + ops: [void 0, "deriveBits"] + }; +} +function aeskw(bits, gcm = !1) { + return { + kty: ["oct"], + mode: "key-wrapping", + secret: !0, + subtle: { name: gcm ? "AES-GCM" : "AES-KW", length: bits }, + usages: none, + ops: gcm ? ["encrypt", "decrypt"] : ["wrapKey", "unwrapKey"] + }; +} +function pbes2() { + return { + kty: ["oct"], + mode: "key-wrapping", + secret: !0, + subtle: { name: "PBKDF2" }, + usages: none, + ops: ["deriveBits", "deriveBits"] + }; +} +const JWE = table({ + dir: { + kty: ["oct"], + mode: "direct-encryption", + secret: !0, + subtle: { name: "AES-GCM" }, + usages: none, + ops: ["encrypt", "decrypt"] + }, + "RSA-OAEP": rsaes(1), + "RSA-OAEP-256": rsaes(256), + "RSA-OAEP-384": rsaes(384), + "RSA-OAEP-512": rsaes(512), + "ECDH-ES": ecdh("direct-key-agreement"), + "ECDH-ES+A128KW": ecdh("key-agreement-with-key-wrapping"), + "ECDH-ES+A192KW": ecdh("key-agreement-with-key-wrapping"), + "ECDH-ES+A256KW": ecdh("key-agreement-with-key-wrapping"), + A128KW: aeskw(128), + A192KW: aeskw(192), + A256KW: aeskw(256), + A128GCMKW: aeskw(128, !0), + A192GCMKW: aeskw(192, !0), + A256GCMKW: aeskw(256, !0), + "PBES2-HS256+A128KW": pbes2(), + "PBES2-HS384+A192KW": pbes2(), + "PBES2-HS512+A256KW": pbes2() +}), contentOps = ["encrypt", "decrypt"]; +function contentEncryption(bits, cbc = !1) { + return { + kty: ["oct"], + secret: !0, + subtle: { name: cbc ? "AES-CBC" : "AES-GCM", length: bits }, + usages: none, + ops: contentOps, + cekBits: bits, + ivBits: cbc ? 128 : 96, + cbc + }; +} +const ENC = table({ + A128GCM: contentEncryption(128), + A192GCM: contentEncryption(192), + A256GCM: contentEncryption(256), + "A128CBC-HS256": contentEncryption(256, !0), + "A192CBC-HS384": contentEncryption(384, !0), + "A256CBC-HS512": contentEncryption(512, !0) +}); +function unsupported(parameter, name) { + throw new JOSENotSupported(`Invalid or unsupported "${parameter}" (JWE ${name}) header value`); +} +function jweAlgorithm(alg) { + return (typeof alg == "string" ? JWE[alg] : void 0) ?? unsupported("alg", "Algorithm"); +} +function isJWECEKTransport(algorithm) { + return algorithm.mode === "key-wrapping" || algorithm.mode === "key-encryption" || algorithm.mode === "key-agreement-with-key-wrapping"; +} +function jweEncryption(enc) { + return (typeof enc == "string" ? ENC[enc] : void 0) ?? unsupported("enc", "Encryption Algorithm"); +} +export { + JWE, + isJWECEKTransport, + jweAlgorithm, + jweEncryption +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_decrypt.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_decrypt.js new file mode 100644 index 0000000000..e5453b9ba9 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_decrypt.js @@ -0,0 +1,187 @@ +import { decrypt, generateCek } from "./content_encryption.js"; +import { decodeBase64url, encodeBase64url, parseJoseHeader, isDisjoint, isObject, validateCrit, validateAlgorithms, JWE_RECOGNIZED } from "./validate.js"; +import { JOSEAlgNotAllowed, JOSENotSupported, JWEInvalid } from "../util/errors.js"; +import { decryptKeyManagement } from "./key_management.js"; +import { decoder } from "./buffer_utils.js"; +import { prepareKey } from "./key.js"; +import { JWE, isJWECEKTransport, jweAlgorithm, jweEncryption } from "./jwe_algorithms.js"; +import { decompress, validateZip } from "./deflate.js"; +function snapshotSharedJWE(jwe) { + const { aad, ciphertext, iv, protected: encodedProtected, tag, unprotected } = jwe; + if (iv !== void 0 && (typeof iv != "string" || !iv)) + throw new JWEInvalid("JWE Initialization Vector incorrect type"); + if (typeof ciphertext != "string") + throw new JWEInvalid("JWE Ciphertext missing or incorrect type"); + if (tag !== void 0 && (typeof tag != "string" || !tag)) + throw new JWEInvalid("JWE Authentication Tag incorrect type"); + if (encodedProtected !== void 0 && typeof encodedProtected != "string") + throw new JWEInvalid("JWE Protected Header incorrect type"); + if (aad !== void 0 && (typeof aad != "string" || !aad)) + throw new JWEInvalid("JWE AAD incorrect type"); + if (unprotected !== void 0 && !isObject(unprotected)) + throw new JWEInvalid("JWE Shared Unprotected Header incorrect type"); + return { + aad, + ciphertext, + iv, + protected: encodedProtected, + tag, + unprotected: unprotected === void 0 ? void 0 : { ...unprotected } + }; +} +function snapshotRecipientJWE(recipient) { + let header, headerAlg; + try { + const { header: inputHeader } = recipient; + if (isObject(inputHeader)) { + headerAlg = inputHeader.alg; + const parameters = Object.keys(inputHeader); + parameters.includes("alg") || (headerAlg = void 0), header = Object.fromEntries(parameters.map((parameter) => [ + parameter, + parameter === "alg" ? headerAlg : inputHeader[parameter] + ])); + } else + header = inputHeader; + const { encrypted_key: encryptedKey } = recipient; + return [{ encrypted_key: encryptedKey, header }, headerAlg]; + } catch (error) { + return [void 0, headerAlg, error]; + } +} +function checkRecipient(jwe) { + const { encrypted_key: encryptedKey, header } = jwe; + if (encryptedKey !== void 0 && typeof encryptedKey != "string") + throw new JWEInvalid("JWE Encrypted Key incorrect type"); + if (header !== void 0 && !isObject(header)) + throw new JWEInvalid("JWE Per-Recipient Unprotected Header incorrect type"); + if (jwe.protected === void 0 && header === void 0 && jwe.unprotected === void 0) + throw new JWEInvalid("JOSE Header missing"); +} +function shareJWE(jwe) { + const { protected: encodedProtected, ciphertext, iv, tag, aad } = jwe; + let parsedProt; + return encodedProtected !== void 0 && (parsedProt = parseJoseHeader(encodedProtected, JWEInvalid, "JWE Protected Header is invalid")), [ + parsedProt, + decodeBase64url(ciphertext, "ciphertext", JWEInvalid), + iv !== void 0 ? decodeBase64url(iv, "iv", JWEInvalid) : void 0, + tag !== void 0 ? decodeBase64url(tag, "tag", JWEInvalid) : void 0, + encodeBase64url((encodedProtected ?? "") + (aad !== void 0 ? `.${aad}` : ""), "aad", JWEInvalid) + ]; +} +function prepareDecrypt(options) { + return [ + options && validateAlgorithms("keyManagementAlgorithms", options.keyManagementAlgorithms), + options && validateAlgorithms("contentEncryptionAlgorithms", options.contentEncryptionAlgorithms), + options?.crit, + options?.maxPBES2Count, + options?.maxDecompressedLength + ]; +} +async function decryptJWE(jwe, shared, key, token = shareJWE(jwe)) { + const [parsedProt, ciphertext, iv, tag, additionalData] = token, { header, unprotected, aad } = jwe; + let joseHeader; + if (header !== void 0 || unprotected !== void 0) { + if (!isDisjoint(parsedProt, header, unprotected)) + throw new JWEInvalid("JWE Protected, JWE Unprotected Header, and JWE Per-Recipient Unprotected Header Parameter names must be disjoint"); + joseHeader = { ...parsedProt, ...header, ...unprotected }; + } else + joseHeader = parsedProt ?? {}; + const [keyManagementAlgorithms, contentEncryptionAlgorithms, crit, maxPBES2Count, maxDecompressedLength] = shared, { encrypted_key: encodedKey } = jwe; + validateCrit(JWEInvalid, JWE_RECOGNIZED, crit, parsedProt, joseHeader), validateZip(joseHeader, parsedProt); + const { alg, enc } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWEInvalid("missing JWE Algorithm (alg) in JWE Header"); + const selected = JWE[alg]; + if (encodedKey === "" && (!selected || !isJWECEKTransport(selected))) + throw new JWEInvalid("JWE Encrypted Key incorrect type"); + const integrated = selected?.mode === "integrated-encryption"; + if (!integrated && (typeof enc != "string" || !enc)) + throw new JWEInvalid("missing JWE Encryption Algorithm (enc) in JWE Header"); + if (keyManagementAlgorithms && !keyManagementAlgorithms.has(alg) || !keyManagementAlgorithms && alg.startsWith("PBES2")) + throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); + let encEntry; + if (integrated) { + if (enc !== void 0) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must not be present for integrated encryption'); + if (iv?.byteLength) + throw new JWEInvalid("JWE Initialization Vector must be empty for integrated encryption"); + if (tag?.byteLength) + throw new JWEInvalid("JWE Authentication Tag must be empty for integrated encryption"); + } else { + if (contentEncryptionAlgorithms && !contentEncryptionAlgorithms.has(enc)) + throw new JOSEAlgNotAllowed('"enc" (Encryption Algorithm) Header Parameter value not allowed'); + encEntry = jweEncryption(enc); + } + let encryptedKey; + if (encodedKey !== void 0) + try { + encryptedKey = decodeBase64url(encodedKey, "encrypted_key", JWEInvalid); + } catch (error) { + if (!selected || !isJWECEKTransport(selected)) + throw error; + encryptedKey = new Uint8Array(); + } + let resolvedKey = !1; + typeof key == "function" && (key = await key(parsedProt, jwe), resolvedKey = !0); + const algEntry = selected ?? jweAlgorithm(alg); + isJWECEKTransport(algEntry) && encryptedKey === void 0 && (encryptedKey = new Uint8Array()); + const k = await prepareKey(algEntry.mode === "direct-encryption" ? encEntry : algEntry, key, "decrypt"); + let plaintext; + if (algEntry.mode === "integrated-encryption") + plaintext = await algEntry.decrypt(k, encryptedKey, ciphertext, additionalData, parsedProt, joseHeader); + else { + const encryption = encEntry; + let cek; + try { + cek = await decryptKeyManagement(algEntry, encryption, k, encryptedKey, joseHeader, maxPBES2Count), isJWECEKTransport(algEntry) && cek instanceof Uint8Array && cek.byteLength << 3 !== encryption.cekBits && (cek = generateCek(encryption)); + } catch (err) { + if (err instanceof TypeError || err instanceof JWEInvalid || err instanceof JOSENotSupported) + throw err; + cek = generateCek(encryption); + } + plaintext = await decrypt(encryption, cek, ciphertext, iv, tag, additionalData); + } + if (joseHeader.zip === "DEF") { + const decompressionLimit = maxDecompressedLength ?? 25e4; + if (decompressionLimit === 0) + throw new JOSENotSupported('JWE "zip" (Compression Algorithm) Header Parameter is not supported.'); + if (decompressionLimit !== 1 / 0 && (!Number.isSafeInteger(decompressionLimit) || decompressionLimit < 1)) + throw new TypeError("maxDecompressedLength must be 0, a positive safe integer, or Infinity"); + plaintext = await decompress(plaintext, decompressionLimit).catch((cause) => { + throw cause instanceof JWEInvalid ? cause : new JWEInvalid("Failed to decompress plaintext", { cause }); + }); + } + return { + plaintext, + ...parsedProt && { protectedHeader: parsedProt }, + ...aad !== void 0 && { + additionalAuthenticatedData: decodeBase64url(aad, "aad", JWEInvalid) + }, + ...unprotected && { sharedUnprotectedHeader: unprotected }, + ...header && { unprotectedHeader: header }, + ...resolvedKey && { key: k } + }; +} +async function decryptCompact(jwe, shared, key) { + if (jwe instanceof Uint8Array && (jwe = decoder.decode(jwe)), typeof jwe != "string") + throw new JWEInvalid("Compact JWE must be a string or Uint8Array"); + const { 0: protectedHeader, 1: encryptedKey, 2: iv, 3: ciphertext, 4: tag, length } = jwe.split("."); + if (length !== 5) + throw new JWEInvalid("Invalid Compact JWE"); + return decryptJWE({ + ciphertext, + iv: iv || void 0, + protected: protectedHeader, + tag: tag || void 0, + encrypted_key: encryptedKey || void 0 + }, shared, key); +} +export { + checkRecipient, + decryptCompact, + decryptJWE, + prepareDecrypt, + shareJWE, + snapshotRecipientJWE, + snapshotSharedJWE +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_encrypt.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_encrypt.js new file mode 100644 index 0000000000..5ab33766b0 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwe_encrypt.js @@ -0,0 +1,75 @@ +import { encode as b64u } from "../util/base64url.js"; +import { encrypt } from "./content_encryption.js"; +import { encryptKeyManagement } from "./key_management.js"; +import { JWEInvalid } from "../util/errors.js"; +import { assertUint8Array, isDisjoint, isObject, serializeJoseHeader, validateCrit, validateCritDuplicates, JWE_RECOGNIZED } from "./validate.js"; +import { encode } from "./buffer_utils.js"; +import { prepareKey } from "./key.js"; +import { JWE, jweAlgorithm, jweEncryption } from "./jwe_algorithms.js"; +import { compress, validateZip } from "./deflate.js"; +function checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader) { + if (!isDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader)) + throw new JWEInvalid("JWE Protected, JWE Shared Unprotected and JWE Per-Recipient Header Parameter names must be disjoint"); +} +function checkEncryptHeaders(input, options, sharedHeadersNormalized = !1) { + if (!input[1] && !input[2] && !input[3]) + throw new JWEInvalid("either setProtectedHeader, setUnprotectedHeader, or sharedUnprotectedHeader must be called before #encrypt()"); + options !== void 0 && (input[8] = options?.crit); + let [, protectedHeader, unprotectedHeader, sharedUnprotectedHeader, aad, cek, iv, keyManagementParameters, crit] = input; + if (aad !== void 0 && assertUint8Array(aad, "JWE Additional Authenticated Data"), cek !== void 0 && assertUint8Array(cek, "JWE Content Encryption Key"), iv !== void 0 && assertUint8Array(iv, "JWE Initialization Vector"), !sharedHeadersNormalized && protectedHeader !== void 0 && (protectedHeader = serializeJoseHeader(JWEInvalid, protectedHeader)[0], input[1] = protectedHeader), unprotectedHeader !== void 0 && (unprotectedHeader = serializeJoseHeader(JWEInvalid, unprotectedHeader)[0], input[2] = unprotectedHeader), !sharedHeadersNormalized && sharedUnprotectedHeader !== void 0 && (sharedUnprotectedHeader = serializeJoseHeader(JWEInvalid, sharedUnprotectedHeader)[0], input[3] = sharedUnprotectedHeader), keyManagementParameters !== void 0 && !isObject(keyManagementParameters)) + throw new TypeError("JWE Key Management Parameters must be an object"); + checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader); + const joseHeader = { + ...protectedHeader, + ...unprotectedHeader, + ...sharedUnprotectedHeader + }; + validateCritDuplicates(JWEInvalid, protectedHeader), validateCrit(JWEInvalid, JWE_RECOGNIZED, crit, protectedHeader, joseHeader), validateZip(joseHeader, protectedHeader); + const { alg, enc } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWEInvalid('JWE "alg" (Algorithm) Header Parameter missing or invalid'); + const algEntry = JWE[alg]; + if (algEntry?.mode === "integrated-encryption") { + if (enc !== void 0) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must not be present for integrated encryption'); + if (cek !== void 0) + throw new TypeError(`setContentEncryptionKey cannot be called with JWE "alg" (Algorithm) Header ${alg}`); + if (iv !== void 0) + throw new TypeError(`setInitializationVector cannot be called with JWE "alg" (Algorithm) Header ${alg}`); + return [joseHeader, void 0, algEntry]; + } + if (typeof enc != "string" || !enc) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter missing or invalid'); + return [joseHeader, jweEncryption(enc), algEntry]; +} +async function encryptJWE(input, checked, key) { + const [joseHeader, encEntry, selected] = checked, [inputPlaintext, inputProtectedHeader, inputUnprotectedHeader, sharedUnprotectedHeader, aad, providedCek, inputIv, keyManagementParameters, , unprotectedParameters] = input; + let protectedHeader = inputProtectedHeader, unprotectedHeader = inputUnprotectedHeader; + const algEntry = selected ?? jweAlgorithm(joseHeader.alg); + let encryptedKey, parameters, cek; + algEntry.mode === "integrated-encryption" ? cek = await prepareKey(algEntry, key, "encrypt") : [cek, encryptedKey, parameters] = await encryptKeyManagement(algEntry, encEntry, key, joseHeader, providedCek, keyManagementParameters), parameters && (unprotectedParameters ? unprotectedHeader = unprotectedHeader ? { ...unprotectedHeader, ...parameters } : parameters : protectedHeader = protectedHeader ? { ...protectedHeader, ...parameters } : parameters, checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader)); + const protectedHeaderS = protectedHeader ? b64u(JSON.stringify(protectedHeader)) : "", aadMember = aad?.byteLength ? b64u(aad) : void 0, additionalData = encode(aadMember ? `${protectedHeaderS}.${aadMember}` : protectedHeaderS); + let plaintext = inputPlaintext; + joseHeader.zip === "DEF" && (plaintext = await compress(plaintext).catch((cause) => { + throw new JWEInvalid("Failed to compress plaintext", { cause }); + })); + let ciphertext, tag, iv; + algEntry.mode === "integrated-encryption" ? [encryptedKey, ciphertext] = await algEntry.encrypt(cek, plaintext, additionalData, protectedHeader, joseHeader, keyManagementParameters) : { ciphertext, tag, iv } = await encrypt(encEntry, plaintext, cek, inputIv, additionalData); + const jwe = { + ciphertext: b64u(ciphertext) + }; + return iv && (jwe.iv = b64u(iv)), tag && (jwe.tag = b64u(tag)), encryptedKey?.byteLength && (jwe.encrypted_key = b64u(encryptedKey)), aadMember && (jwe.aad = aadMember), protectedHeader && (jwe.protected = protectedHeaderS), sharedUnprotectedHeader && (jwe.unprotected = sharedUnprotectedHeader), unprotectedHeader && (jwe.header = unprotectedHeader), jwe; +} +async function createJWE(input, key, options) { + return encryptJWE(input, checkEncryptHeaders(input, options), key); +} +function compactJWE(jwe) { + return [jwe.protected, jwe.encrypted_key, jwe.iv, jwe.ciphertext, jwe.tag].join("."); +} +export { + checkDisjoint, + checkEncryptHeaders, + compactJWE, + createJWE, + encryptJWE +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwt_claims_set.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwt_claims_set.js new file mode 100644 index 0000000000..b99943f9cd --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/jwt_claims_set.js @@ -0,0 +1,146 @@ +import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from "../util/errors.js"; +import { encoder, strictDecoder } from "./buffer_utils.js"; +import { isObject } from "./validate.js"; +const epoch = (date) => Math.floor(date.getTime() / 1e3), multipliers = { + s: 1, + m: 60, + h: 3600, + d: 86400, + w: 604800, + y: 31557600 +}, REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i, checkFailed = "check_failed"; +function invalidDuration() { + throw new TypeError("Invalid time period format"); +} +function secs(str) { + typeof str != "string" && invalidDuration(); + const matched = REGEX.exec(str); + (!matched || matched[4] && matched[1]) && invalidDuration(); + const value = parseFloat(matched[2]), numericDate2 = Math.round(value * multipliers[matched[3][0].toLowerCase()]); + return Number.isFinite(numericDate2) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -numericDate2 : numericDate2; +} +function validateInput(label, input) { + if (!Number.isFinite(input)) + throw new TypeError(`Invalid ${label} input`); + return input; +} +function validateStringClaim(claim, value) { + if (typeof value != "string") + throw new TypeError(`"${claim}" claim must be a string`); +} +function validateAudienceClaim(value) { + if (typeof value != "string" && (!Array.isArray(value) || Array.from(value).some((member) => typeof member != "string"))) + throw new TypeError('"aud" claim must be a string or an array of strings'); +} +function numericDate(value, label) { + return typeof value == "number" ? validateInput(label, value) : value instanceof Date ? validateInput(label, epoch(value)) : epoch(/* @__PURE__ */ new Date()) + secs(value); +} +const normalizeTyp = (value) => { + const normalized = value.toLowerCase(); + return value.includes("/") ? normalized : `application/${normalized}`; +}, checkAudiencePresence = (audPayload, audOption) => typeof audPayload == "string" ? audOption.includes(audPayload) : Array.isArray(audPayload) ? audOption.some((aud) => audPayload.includes(aud)) : !1; +function validateNumericDate(payload, claim, required = !1) { + const value = payload[claim]; + if (!(value === void 0 && !required)) { + if (typeof value != "number") + throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, payload, claim, "invalid"); + return value; + } +} +function unexpectedClaim(payload, claim) { + throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, payload, claim, checkFailed); +} +function validateClaimsSet(protectedHeader, encodedPayload, options = {}) { + let payload; + try { + payload = JSON.parse(strictDecoder.decode(encodedPayload)); + } catch { + } + if (!isObject(payload)) + throw new JWTInvalid("JWT Claims Set must be a top-level JSON object"); + const { typ } = options; + if (typ !== void 0 && (typeof protectedHeader.typ != "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) + throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, "typ", checkFailed); + const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options, presenceCheck = [...requiredClaims]; + maxTokenAge !== void 0 && presenceCheck.push("iat"), audience !== void 0 && presenceCheck.push("aud"), subject !== void 0 && presenceCheck.push("sub"), issuer !== void 0 && presenceCheck.push("iss"); + for (const claim of new Set(presenceCheck.reverse())) + if (!Object.hasOwn(payload, claim)) + throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing"); + issuer !== void 0 && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss) && unexpectedClaim(payload, "iss"), subject !== void 0 && payload.sub !== subject && unexpectedClaim(payload, "sub"), audience !== void 0 && !checkAudiencePresence(payload.aud, typeof audience == "string" ? [audience] : audience) && unexpectedClaim(payload, "aud"); + const { clockTolerance } = options; + let tolerance = 0; + if (typeof clockTolerance == "string") + tolerance = secs(clockTolerance); + else if (clockTolerance !== void 0) { + if (typeof clockTolerance != "number") + throw new TypeError("Invalid clockTolerance option type"); + tolerance = clockTolerance; + } + validateInput("clockTolerance option", tolerance); + const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === void 0 ? /* @__PURE__ */ new Date() : currentDate)), iat = validateNumericDate(payload, "iat", maxTokenAge !== void 0), nbf = validateNumericDate(payload, "nbf"); + if (nbf !== void 0 && nbf > now + tolerance) + throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", checkFailed); + const exp = validateNumericDate(payload, "exp"); + if (exp !== void 0 && exp <= now - tolerance) + throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", checkFailed); + if (maxTokenAge !== void 0) { + const age = now - iat, max = validateInput("maxTokenAge option", typeof maxTokenAge == "number" ? maxTokenAge : secs(maxTokenAge)); + if (age - tolerance > max) + throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, "iat", checkFailed); + if (age < -tolerance) + throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, "iat", checkFailed); + } + return payload; +} +let producerPayloads; +function producerPayload(producer) { + return producerPayloads.get(producer); +} +function jwtData(producer) { + const payload = producerPayload(producer); + for (const claim of ["iat", "nbf", "exp"]) { + const value = payload[claim]; + if (typeof value == "number" && !Number.isFinite(value)) + throw new TypeError(`"${claim}" claim must be a finite number`); + } + return encoder.encode(JSON.stringify(payload)); +} +function jwtClaim(producer, claim) { + return producerPayload(producer)[claim]; +} +class JWTClaimsBuilder { + constructor(payload = {}) { + if (!isObject(payload)) + throw new TypeError("JWT Claims Set MUST be an object"); + (producerPayloads ||= /* @__PURE__ */ new WeakMap()).set(this, structuredClone(payload)); + } + setIssuer(value) { + return validateStringClaim("iss", value), producerPayload(this).iss = value, this; + } + setSubject(value) { + return validateStringClaim("sub", value), producerPayload(this).sub = value, this; + } + setAudience(value) { + return validateAudienceClaim(value), producerPayload(this).aud = value, this; + } + setJti(value) { + return validateStringClaim("jti", value), producerPayload(this).jti = value, this; + } + setNotBefore(value) { + return producerPayload(this).nbf = numericDate(value, "setNotBefore"), this; + } + setExpirationTime(value) { + return producerPayload(this).exp = numericDate(value, "setExpirationTime"), this; + } + setIssuedAt(value) { + const payload = producerPayload(this); + return value === void 0 ? payload.iat = epoch(/* @__PURE__ */ new Date()) : typeof value == "string" ? payload.iat = validateInput("setIssuedAt", epoch(/* @__PURE__ */ new Date()) + secs(value)) : payload.iat = numericDate(value, "setIssuedAt"), this; + } +} +export { + JWTClaimsBuilder, + jwtClaim, + jwtData, + secs, + validateClaimsSet +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key.js new file mode 100644 index 0000000000..7dc5602f10 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key.js @@ -0,0 +1,155 @@ +import { isObject } from "./validate.js"; +import { decode } from "../util/base64url.js"; +import { JOSENotSupported } from "../util/errors.js"; +const tag = (key) => key[Symbol.toStringTag], jwkMatchesOp = (entry, key, usage) => { + const { alg } = entry; + if (key.use !== void 0) { + const expected = usage === "sign" || usage === "verify" ? "sig" : "enc"; + if (key.use !== expected) + throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`); + } + if (key.alg !== void 0 && key.alg !== alg) + throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`); + if (Array.isArray(key.key_ops)) { + const expectedKeyOp = usage === "encrypt" || usage === "decrypt" ? entry.ops?.[usage === "encrypt" ? 0 : 1] : usage; + if (expectedKeyOp && !key.key_ops.includes(expectedKeyOp)) + throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`); + } +}; +async function prepareKey(entry, key, usage) { + const { alg, secret } = entry, privateKey = usage === "decrypt" || usage === "sign"; + if (secret && key instanceof Uint8Array) + return key; + let normalized, keyObject; + if (isObject(key)) { + if (normalized = normalizeJwk(key), typeof normalized.kty != "string") + throw invalidKeyType(alg, key, secret); + if (!(secret ? normalized.kty === "oct" && typeof normalized.k == "string" : normalized.kty !== "oct" && (privateKey ? normalized.kty === "AKP" && typeof normalized.priv == "string" || typeof normalized.d == "string" : normalized.d === void 0 && normalized.priv === void 0))) + throw new TypeError(secret ? 'JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present' : `JSON Web Key for this operation must be a ${privateKey ? "private" : "public"} JWK`); + if (jwkMatchesOp(entry, normalized, usage), normalized.kty === "oct") + return decode(normalized.k); + if (!Object.isFrozen(key)) { + const { key_ops } = key; + Array.isArray(key_ops) && Object.freeze(key_ops), Object.freeze(key); + } + } else { + if (!isKeyLike(key)) + throw invalidKeyType(alg, key, secret); + const expectedType = secret ? "secret" : privateKey ? "private" : "public"; + if (key.type !== expectedType && (secret || ["secret", "public", "private"].includes(key.type))) + throw new TypeError(`${tag(key)} instances must be of type "${expectedType}" for the ${alg} algorithm`); + if (isCryptoKey(key)) + return key; + if (keyObject = key, keyObject.type === "secret") + return keyObject.export(); + } + cache ||= /* @__PURE__ */ new WeakMap(); + const cacheKey = key; + let cached = cache.get(cacheKey); + if (cached?.[alg]) + return cached[alg]; + if (cached || cache.set(cacheKey, cached = {}), keyObject && typeof keyObject.toCryptoKey == "function") { + const isPublic = keyObject.type === "public", crv = nist[keyObject.asymmetricKeyDetails?.namedCurve], params = entry.resolve?.({ crv, asymmetricKeyType: keyObject.asymmetricKeyType }) ?? entry.subtle; + return cached[alg] = keyObject.toCryptoKey(params, isPublic, entry.usages[isPublic ? 0 : 1]); + } + return normalized ??= keyObject.export({ format: "jwk" }), normalized.alg = alg, cached[alg] = await jwkToKey(entry, normalized); +} +let cache; +const nist = { + __proto__: null, + prime256v1: "P-256", + secp384r1: "P-384", + secp521r1: "P-521" +}; +function assertCryptoKey(key) { + if (!isCryptoKey(key)) + throw new Error("CryptoKey instance expected"); +} +const isCryptoKey = (key) => { + if (key?.[Symbol.toStringTag] === "CryptoKey") + return !0; + try { + return key instanceof CryptoKey; + } catch { + return !1; + } +}, isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject", isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key); +function message(msg, actual, ...types) { + if (types.length > 2) { + const last = types.pop(); + msg += `one of type ${types.join(", ")}, or ${last}.`; + } else types.length === 2 ? msg += `one of type ${types[0]} or ${types[1]}.` : msg += `of type ${types[0]}.`; + return actual == null ? msg += ` Received ${actual}` : typeof actual == "function" && actual.name ? msg += ` Received function ${actual.name}` : typeof actual == "object" && actual != null && actual.constructor?.name && (msg += ` Received an instance of ${actual.constructor.name}`), msg; +} +const invalidKeyInput = (actual, ...types) => message("Key must be ", actual, ...types); +function invalidKeyType(alg, actual, secret) { + const types = ["CryptoKey", "KeyObject", "JSON Web Key"]; + return secret && types.push("Uint8Array"), new TypeError(message(`Key for the ${alg} algorithm must be `, actual, ...types)); +} +const unusable = (name, prop = "algorithm.name") => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`); +function checkUsage(key, usage) { + if (usage && !key.usages.includes(usage)) + throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`); +} +function checkModulusLength(alg, key) { + const { modulusLength } = key.algorithm; + if (typeof modulusLength != "number" || modulusLength < 2048) + throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`); +} +function checkCryptoKey(key, expected, usage) { + const algorithm = key.algorithm; + if (algorithm.name !== expected.name) + throw unusable(expected.name); + if (expected.hash && algorithm.hash?.name !== expected.hash) + throw unusable(expected.hash, "algorithm.hash"); + if (expected.namedCurve && algorithm.namedCurve !== expected.namedCurve) + throw unusable(expected.namedCurve, "algorithm.namedCurve"); + if (expected.length !== void 0 && algorithm.length !== expected.length) + throw unusable(expected.length, "algorithm.length"); + checkUsage(key, usage); +} +function snapshotJwk(jwk) { + return { __proto__: null, ...jwk }; +} +function normalizeJwk(jwk) { + const normalized = snapshotJwk(jwk); + if (normalized.ext !== void 0 && typeof normalized.ext != "boolean") + throw new TypeError('"ext" (Extractable) Parameter must be a boolean'); + if (normalized.key_ops !== void 0) { + const value = normalized.key_ops, keyOps = Array.isArray(value) ? [...value] : void 0; + if (!keyOps || keyOps.some((operation) => typeof operation != "string") || new Set(keyOps).size !== keyOps.length) + throw new TypeError('"key_ops" (Key Operations) Parameter must be an array of unique strings'); + normalized.key_ops = keyOps; + } + return normalized; +} +function validateExtractableOption(extractable) { + if (extractable !== void 0 && typeof extractable != "boolean") + throw new TypeError('"extractable" option must be a boolean'); + return extractable; +} +async function jwkToKey(entry, jwk, extractable) { + if (!entry.kty.includes(jwk.kty)) + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + const algorithm = entry.resolve?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle, isPrivate = !!(jwk.d || jwk.priv), keyData = { ...jwk, ext: extractable ?? jwk.ext }; + return keyData.kty !== "AKP" && delete keyData.alg, delete keyData.use, crypto.subtle.importKey("jwk", keyData, algorithm, keyData.ext ?? !isPrivate, jwk.key_ops ?? entry.usages[isPrivate ? 1 : 0]); +} +async function rawKey(key, expected, usage, extractable = !1) { + return key instanceof Uint8Array && (key = await crypto.subtle.importKey("raw", key, expected, extractable, [usage])), checkCryptoKey(key, expected, usage), key; +} +export { + assertCryptoKey, + checkCryptoKey, + checkModulusLength, + checkUsage, + invalidKeyInput, + isCryptoKey, + isKeyLike, + isKeyObject, + jwkToKey, + normalizeJwk, + prepareKey, + rawKey, + snapshotJwk, + validateExtractableOption +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key_descriptor.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key_descriptor.js new file mode 100644 index 0000000000..cf0afbd90f --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key_descriptor.js @@ -0,0 +1,9 @@ +function table(entries) { + const out = { __proto__: null }; + for (const alg in entries) + out[alg] = { ...entries[alg], alg }; + return out; +} +export { + table +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key_management.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key_management.js new file mode 100644 index 0000000000..7b7533b706 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/key_management.js @@ -0,0 +1,196 @@ +import { encode as b64u } from "../util/base64url.js"; +import { prepareKey, rawKey, jwkToKey, checkCryptoKey, checkModulusLength, checkUsage, assertCryptoKey } from "./key.js"; +import { jweAlgorithm, jweEncryption, isJWECEKTransport } from "./jwe_algorithms.js"; +import { JOSENotSupported, JWEInvalid } from "../util/errors.js"; +import { decodeBase64url, assertUint8Array, isObject } from "./validate.js"; +import { digest, concat, encode, uint32be } from "./buffer_utils.js"; +import { checkCekLength, generateCek, encrypt, decrypt } from "./content_encryption.js"; +function checkEcdhCryptoKey(key, usage) { + if (key.algorithm.name !== "ECDH" && key.algorithm.name !== "X25519") + throw new TypeError("CryptoKey does not support this operation, its algorithm.name must be ECDH or X25519"); + checkUsage(key, usage); +} +async function aeskwWrap(alg, key, cek) { + const cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "wrapKey", !0), cryptoKeyCek = await crypto.subtle.importKey("raw", cek, { hash: "SHA-256", name: "HMAC" }, !0, ["sign"]); + return new Uint8Array(await crypto.subtle.wrapKey("raw", cryptoKeyCek, cryptoKey, "AES-KW")); +} +async function aeskwUnwrap(alg, key, encryptedKey) { + const cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "unwrapKey", !0), cryptoKeyCek = await crypto.subtle.unwrapKey("raw", encryptedKey, cryptoKey, "AES-KW", { hash: "SHA-256", name: "HMAC" }, !0, ["sign"]); + return new Uint8Array(await crypto.subtle.exportKey("raw", cryptoKeyCek)); +} +function checkRsaKey(alg, key, usage) { + checkCryptoKey(key, jweAlgorithm(alg).subtle, usage), checkModulusLength(alg, key); +} +async function deriveKey(p2s, alg, p2c, key) { + if (!(p2s instanceof Uint8Array) || p2s.length < 8) + throw new JWEInvalid("PBES2 Salt Input must be 8 or more octets"); + if (!Number.isSafeInteger(p2c) || Math.sign(p2c) !== 1) + throw new JWEInvalid("PBES2 Count Input must be a positive integer"); + const salt = concat(encode(alg), Uint8Array.of(0), p2s), keylen = parseInt(alg.slice(13, 16), 10), subtleAlg = { + hash: `SHA-${alg.slice(8, 11)}`, + iterations: p2c, + name: "PBKDF2", + salt + }, cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "deriveBits"); + return new Uint8Array(await crypto.subtle.deriveBits(subtleAlg, cryptoKey, keylen)); +} +function lengthAndInput(input) { + return concat(uint32be(input.length), input); +} +async function concatKdf(Z, L, OtherInfo) { + const dkLen = L >> 3, hashLen = 32, reps = Math.ceil(dkLen / hashLen), dk = new Uint8Array(reps * hashLen); + for (let i = 1; i <= reps; i++) { + const hashResult = await digest("sha256", concat(uint32be(i), Z, OtherInfo)); + dk.set(hashResult, (i - 1) * hashLen); + } + return dk.slice(0, dkLen); +} +async function ecdhesDeriveKey(publicKey, privateKey, algorithm, keyLength, apu = new Uint8Array(), apv = new Uint8Array()) { + checkEcdhCryptoKey(publicKey), checkEcdhCryptoKey(privateKey, "deriveBits"); + const otherInfo = concat(lengthAndInput(encode(algorithm)), lengthAndInput(apu), lengthAndInput(apv), uint32be(keyLength)), Z = new Uint8Array(await crypto.subtle.deriveBits({ + name: publicKey.algorithm.name, + public: publicKey + }, privateKey, publicKey.algorithm.name === "X25519" ? 256 : Math.ceil(parseInt(publicKey.algorithm.namedCurve.slice(-3), 10) / 8) << 3)); + return concatKdf(Z, keyLength, otherInfo); +} +function assertEcdhKey(key) { + assertCryptoKey(key); + const curve = key.algorithm.namedCurve; + if (curve !== "P-256" && curve !== "P-384" && curve !== "P-521" && key.algorithm.name !== "X25519") + throw new JOSENotSupported("ECDH with the provided key is not allowed or not supported by your javascript runtime"); +} +function partyInfo(joseHeader, name) { + const value = joseHeader[name]; + if (value !== void 0) { + if (typeof value != "string") + throw new JWEInvalid(`JOSE Header "${name}" (Agreement Party${name === "apu" ? "U" : "V"}Info) invalid`); + return decodeBase64url(value, name, JWEInvalid); + } +} +function checkPartyInfo(apu, apv) { + if (!(apu === void 0 || apv === void 0 || apu.byteLength !== apv.byteLength)) { + for (let i = 0; i < apu.byteLength; i++) + if (apu[i] !== apv[i]) + return; + throw new JWEInvalid('JOSE Header "apu" and "apv" values must be distinct'); + } +} +function assertEncryptedKey(encryptedKey) { + if (encryptedKey === void 0) + throw new JWEInvalid("JWE Encrypted Key missing"); +} +function assertNoEncryptedKey(encryptedKey) { + if (encryptedKey !== void 0) + throw new JWEInvalid("Encountered unexpected JWE Encrypted Key"); +} +function validateMaxPBES2Count(value) { + if (value !== void 0 && value !== 1 / 0 && (!Number.isSafeInteger(value) || value < 1)) + throw new TypeError("maxPBES2Count must be a positive safe integer or Infinity"); +} +async function decryptKeyManagement(entry, enc, key, encryptedKey, joseHeader, maxPBES2Count) { + const { alg } = entry, mode = entry.mode; + if (mode === "direct-encryption") + return assertNoEncryptedKey(encryptedKey), key; + const direct = mode === "direct-key-agreement"; + switch (direct ? assertNoEncryptedKey(encryptedKey) : assertEncryptedKey(encryptedKey), entry.subtle.name) { + case "ECDH": { + const { epk } = joseHeader; + if (!isObject(epk) || ["d", "k", "p", "q", "dp", "dq", "qi", "oth", "priv"].some((parameter) => Object.hasOwn(epk, parameter))) + throw new JWEInvalid('JOSE Header "epk" (Ephemeral Public Key) missing or invalid'); + assertEcdhKey(key); + const ephemeralPublicKey = await jwkToKey(entry, epk), partyUInfo = partyInfo(joseHeader, "apu"), partyVInfo = partyInfo(joseHeader, "apv"); + checkPartyInfo(partyUInfo, partyVInfo); + const sharedSecret = await ecdhesDeriveKey(ephemeralPublicKey, key, direct ? enc.alg : alg, direct ? enc.cekBits : parseInt(alg.slice(-5, -2), 10), partyUInfo, partyVInfo); + if (direct) + return sharedSecret; + key = sharedSecret; + break; + } + case "RSA-OAEP": + return assertCryptoKey(key), checkRsaKey(alg, key, "decrypt"), new Uint8Array(await crypto.subtle.decrypt("RSA-OAEP", key, encryptedKey)); + case "PBKDF2": { + if (typeof joseHeader.p2c != "number") + throw new JWEInvalid('JOSE Header "p2c" (PBES2 Count) missing or invalid'); + validateMaxPBES2Count(maxPBES2Count); + const p2cLimit = maxPBES2Count ?? 1e4; + if (joseHeader.p2c > p2cLimit) + throw new JWEInvalid('JOSE Header "p2c" (PBES2 Count) out is of acceptable bounds'); + if (typeof joseHeader.p2s != "string") + throw new JWEInvalid('JOSE Header "p2s" (PBES2 Salt) missing or invalid'); + const p2s = decodeBase64url(joseHeader.p2s, "p2s", JWEInvalid); + key = await deriveKey(p2s, alg, joseHeader.p2c, key); + break; + } + case "AES-GCM": { + if (typeof joseHeader.iv != "string") + throw new JWEInvalid('JOSE Header "iv" (Initialization Vector) missing or invalid'); + if (typeof joseHeader.tag != "string") + throw new JWEInvalid('JOSE Header "tag" (Authentication Tag) missing or invalid'); + const iv = decodeBase64url(joseHeader.iv, "iv", JWEInvalid), tag = decodeBase64url(joseHeader.tag, "tag", JWEInvalid); + if (iv.byteLength !== 12) + throw new JWEInvalid("Invalid Initialization Vector length"); + if (tag.byteLength !== 16) + throw new JWEInvalid("Invalid Authentication Tag length"); + return decrypt(jweEncryption(alg.slice(0, -2)), key, encryptedKey, iv, tag, new Uint8Array()); + } + } + return aeskwUnwrap(alg.slice(-6), key, encryptedKey); +} +async function encryptKeyManagement(entry, enc, inputKey, joseHeader, providedCek, providedParameters = {}) { + const { alg, mode } = entry, transport = isJWECEKTransport(entry); + if (providedCek !== void 0 && !transport) + throw new TypeError(`setContentEncryptionKey cannot be called with JWE "alg" (Algorithm) Header ${alg}`); + let key = await prepareKey(mode === "direct-encryption" ? enc : entry, inputKey, "encrypt"); + if (mode === "direct-encryption") + return [key, void 0, void 0]; + const cek = transport ? providedCek ?? generateCek(enc) : void 0; + cek && checkCekLength(cek, enc.cekBits); + let encryptedKey, parameters; + switch (entry.subtle.name) { + case "ECDH": { + assertEcdhKey(key); + const { apu: providedApu, apv: providedApv } = providedParameters; + providedApu !== void 0 && assertUint8Array(providedApu, '"apu"'), providedApv !== void 0 && assertUint8Array(providedApv, '"apv"'); + const apu = providedApu ?? partyInfo(joseHeader, "apu"), apv = providedApv ?? partyInfo(joseHeader, "apv"); + checkPartyInfo(apu, apv); + let ephemeralKey; + providedParameters.epk !== void 0 ? ephemeralKey = await prepareKey(entry, providedParameters.epk, "decrypt") : ephemeralKey = (await crypto.subtle.generateKey(key.algorithm, !0, ["deriveBits"])).privateKey; + const subtle = crypto.subtle; + let exportableEpk = ephemeralKey; + if (!exportableEpk.extractable) { + if (typeof subtle.getPublicKey != "function") + throw new TypeError('CryptoKey for "epk" must be extractable'); + exportableEpk = await subtle.getPublicKey(ephemeralKey, []); + } + const { x, y, crv, kty } = await subtle.exportKey("jwk", exportableEpk), direct = mode === "direct-key-agreement", sharedSecret = await ecdhesDeriveKey(key, ephemeralKey, direct ? enc.alg : alg, direct ? enc.cekBits : parseInt(alg.slice(-5, -2), 10), apu, apv), epk = { x, crv, kty }; + if (kty === "EC" && (epk.y = y), parameters = { epk }, providedApu !== void 0 && (parameters.apu = b64u(providedApu)), providedApv !== void 0 && (parameters.apv = b64u(providedApv)), direct) + return [sharedSecret, void 0, parameters]; + key = sharedSecret; + break; + } + case "RSA-OAEP": { + assertCryptoKey(key), checkRsaKey(alg, key, "encrypt"), encryptedKey = new Uint8Array(await crypto.subtle.encrypt("RSA-OAEP", key, cek)); + break; + } + case "PBKDF2": { + const { p2c = 2048, p2s = crypto.getRandomValues(new Uint8Array(16)) } = providedParameters; + key = await deriveKey(p2s, alg, p2c, key), parameters = { p2c, p2s: b64u(p2s) }; + break; + } + case "AES-GCM": { + const iv = providedParameters.iv === void 0 ? crypto.getRandomValues(new Uint8Array(12)) : providedParameters.iv; + if (!(iv instanceof Uint8Array)) + throw new TypeError('"iv" must be an instance of Uint8Array'); + const wrapped = await encrypt(jweEncryption(alg.slice(0, -2)), cek, key, iv, new Uint8Array()); + encryptedKey = wrapped.ciphertext, parameters = { iv: b64u(wrapped.iv), tag: b64u(wrapped.tag) }; + } + } + if (encryptedKey ??= await aeskwWrap(alg.slice(-6), key, cek), !(encryptedKey instanceof Uint8Array) || !encryptedKey.byteLength) + throw new TypeError("JWE key management algorithm did not produce an Encrypted Key"); + return [cek, encryptedKey, parameters]; +} +export { + decryptKeyManagement, + encryptKeyManagement, + validateMaxPBES2Count +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/validate.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/validate.js new file mode 100644 index 0000000000..51b7dfe896 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/lib/validate.js @@ -0,0 +1,122 @@ +import { JOSENotSupported, JWSInvalid } from "../util/errors.js"; +import { decode } from "../util/base64url.js"; +import { encode, strictDecoder } from "./buffer_utils.js"; +function assertUint8Array(input, label) { + if (!(input instanceof Uint8Array)) + throw new TypeError(`${label} must be an instance of Uint8Array`); +} +function isObject(input) { + if (typeof input != "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]") + return !1; + const prototype = Object.getPrototypeOf(input); + return prototype === null || Object.getPrototypeOf(prototype) === null; +} +function isJwkSet(input) { + return isObject(input) && Array.isArray(input.keys) && Array.from(input.keys).every(isObject); +} +function isDisjoint(...headers) { + const parameters = /* @__PURE__ */ new Set(); + for (const header of headers) + if (header) + for (const parameter of Object.keys(header)) { + if (parameters.has(parameter)) + return !1; + parameters.add(parameter); + } + return !0; +} +function assertNotSet(value, name) { + if (value !== void 0) + throw new TypeError(`${name} can only be called once`); +} +function decodeBase64url(value, label, ErrorClass) { + try { + return decode(value); + } catch { + throw new ErrorClass(`Failed to base64url decode the ${label}`); + } +} +function encodeBase64url(value, label, ErrorClass) { + try { + return encode(value); + } catch { + throw new ErrorClass(`The ${label} is not a valid base64url string`); + } +} +function parseJoseHeader(b64, ErrorClass, message) { + let parsed; + try { + parsed = JSON.parse(strictDecoder.decode(decode(b64))); + } catch { + throw new ErrorClass(message); + } + if (!isObject(parsed)) + throw new ErrorClass(message); + return parsed; +} +const JWS_RECOGNIZED = { __proto__: null, b64: !0 }, JWE_RECOGNIZED = { __proto__: null }; +function validateAlgorithms(option, algorithms) { + if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s != "string"))) + throw new TypeError(`"${option}" option must be an array of strings`); + return algorithms === void 0 ? void 0 : new Set(algorithms); +} +function validateCritDuplicates(Err, protectedHeader) { + const { crit } = protectedHeader ?? {}; + if (Array.isArray(crit) && new Set(crit).size !== crit.length) + throw new Err('"crit" (Critical) Header Parameter MUST NOT contain duplicate values'); +} +function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) { + if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0) + throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected'); + if (!protectedHeader || protectedHeader.crit === void 0) + return []; + if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input != "string" || input.length === 0)) + throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present'); + const recognized = recognizedOption === void 0 ? recognizedDefault : { __proto__: null, ...recognizedOption, ...recognizedDefault }; + for (const parameter of protectedHeader.crit) { + if (!(parameter in recognized)) + throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`); + if (!Object.hasOwn(joseHeader, parameter) || joseHeader[parameter] === void 0) + throw new Err(`Extension Header Parameter "${parameter}" is missing`); + if (recognized[parameter] && (!Object.hasOwn(protectedHeader, parameter) || protectedHeader[parameter] === void 0)) + throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`); + } + return protectedHeader.crit; +} +function validateB64(protectedHeader, extensions) { + if (extensions.includes("b64")) { + const b64 = protectedHeader.b64; + if (typeof b64 != "boolean") + throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean'); + return b64; + } + return !0; +} +function serializeJoseHeader(Err, header) { + let serialized, parsed; + try { + serialized = JSON.stringify(header), parsed = JSON.parse(serialized); + } catch (cause) { + throw new Err("JOSE Header is not valid JSON", { cause }); + } + if (!isObject(parsed)) + throw new Err("JOSE Header is not a JSON object"); + return [parsed, serialized]; +} +export { + JWE_RECOGNIZED, + JWS_RECOGNIZED, + assertNotSet, + assertUint8Array, + decodeBase64url, + encodeBase64url, + isDisjoint, + isJwkSet, + isObject, + parseJoseHeader, + serializeJoseHeader, + validateAlgorithms, + validateB64, + validateCrit, + validateCritDuplicates +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/util/base64url.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/util/base64url.js new file mode 100644 index 0000000000..4447baa6d2 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/util/base64url.js @@ -0,0 +1,16 @@ +import { encoder, decoder, encodeBase64, decodeBase64 } from "../lib/buffer_utils.js"; +const invalid = "The input to be decoded is not correctly encoded."; +function decode(input) { + try { + return decodeBase64(typeof input == "string" ? input : decoder.decode(input), !0); + } catch (cause) { + throw new TypeError(invalid, { cause }); + } +} +function encode(input) { + return encodeBase64(typeof input == "string" ? encoder.encode(input) : input, !0); +} +export { + decode, + encode +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/util/errors.js b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/util/errors.js new file mode 100644 index 0000000000..b7d579f519 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/dist/webapi/util/errors.js @@ -0,0 +1,109 @@ +class JOSEError extends Error { + static code = "ERR_JOSE_GENERIC"; + code = "ERR_JOSE_GENERIC"; + constructor(message, options) { + super(message, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor); + } +} +class JWTClaimValidationFailed extends JOSEError { + static code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JWTExpired extends JOSEError { + static code = "ERR_JWT_EXPIRED"; + code = "ERR_JWT_EXPIRED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JOSEAlgNotAllowed extends JOSEError { + static code = "ERR_JOSE_ALG_NOT_ALLOWED"; + code = "ERR_JOSE_ALG_NOT_ALLOWED"; +} +class JOSENotSupported extends JOSEError { + static code = "ERR_JOSE_NOT_SUPPORTED"; + code = "ERR_JOSE_NOT_SUPPORTED"; +} +class JWEDecryptionFailed extends JOSEError { + static code = "ERR_JWE_DECRYPTION_FAILED"; + code = "ERR_JWE_DECRYPTION_FAILED"; + constructor(message = "decryption operation failed", options) { + super(message, options); + } +} +class JWEInvalid extends JOSEError { + static code = "ERR_JWE_INVALID"; + code = "ERR_JWE_INVALID"; +} +class JWSInvalid extends JOSEError { + static code = "ERR_JWS_INVALID"; + code = "ERR_JWS_INVALID"; +} +class JWTInvalid extends JOSEError { + static code = "ERR_JWT_INVALID"; + code = "ERR_JWT_INVALID"; +} +class JWKInvalid extends JOSEError { + static code = "ERR_JWK_INVALID"; + code = "ERR_JWK_INVALID"; +} +class JWKSInvalid extends JOSEError { + static code = "ERR_JWKS_INVALID"; + code = "ERR_JWKS_INVALID"; +} +class JWKSNoMatchingKey extends JOSEError { + static code = "ERR_JWKS_NO_MATCHING_KEY"; + code = "ERR_JWKS_NO_MATCHING_KEY"; + constructor(message = "no applicable key found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSMultipleMatchingKeys extends JOSEError { + [Symbol.asyncIterator] = async function* () { + }; + static code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + constructor(message = "multiple matching keys found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSTimeout extends JOSEError { + static code = "ERR_JWKS_TIMEOUT"; + code = "ERR_JWKS_TIMEOUT"; + constructor(message = "request timed out", options) { + super(message, options); + } +} +class JWSSignatureVerificationFailed extends JOSEError { + static code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + constructor(message = "signature verification failed", options) { + super(message, options); + } +} +export { + JOSEAlgNotAllowed, + JOSEError, + JOSENotSupported, + JWEDecryptionFailed, + JWEInvalid, + JWKInvalid, + JWKSInvalid, + JWKSMultipleMatchingKeys, + JWKSNoMatchingKey, + JWKSTimeout, + JWSInvalid, + JWSSignatureVerificationFailed, + JWTClaimValidationFailed, + JWTExpired, + JWTInvalid +}; diff --git a/tests/corpus/7132-jose-pbes2/node_modules/jose/package.json b/tests/corpus/7132-jose-pbes2/node_modules/jose/package.json new file mode 100644 index 0000000000..bcb3534304 --- /dev/null +++ b/tests/corpus/7132-jose-pbes2/node_modules/jose/package.json @@ -0,0 +1,17 @@ +{ + "name": "jose", + "version": "6.2.12", + "license": "MIT", + "type": "module", + "sideEffects": false, + "exports": { + "./jwt/*": { + "types": "./dist/types/jwt/*.d.ts", + "default": "./dist/webapi/jwt/*.js" + }, + "./errors": { + "types": "./dist/types/util/errors.d.ts", + "default": "./dist/webapi/util/errors.js" + } + } +} diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 274d572724..27c212daa1 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -101,11 +101,11 @@ createECDH("sect163k1"); createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); /* WebCrypto beyond the secret-key slice: @types/node declares the whole * SubtleCrypto surface, and the methods outside the slice, such as - * wrapKey, fence by name. */ -async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); + * getPublicKey, fence by name. */ +async function publicKeyWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + await crypto.subtle.getPublicKey(key, ["verify"]); } -void wrapWithWebCrypto; +void publicKeyWithWebCrypto; /* Node's navigator global: typeof navigator and navigator.userAgent * compile; the other members fence by name. */ const cores = navigator.hardwareConcurrency; diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 3198db3660..ce43c50855 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -223,14 +223,14 @@ fenced.ts:101:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer hint: Node reads the second argument as the key's input encoding and answers a Buffer; pass the output encoding third: computeSecret(key, undefined, encoding) -fenced.ts:106:9 - error SC2020: 'crypto.subtle.wrapKey' is part of the standard library types but has no scriptc lowering yet +fenced.ts:106:9 - error SC2020: 'crypto.subtle.getPublicKey' is part of the standard library types but has no scriptc lowering yet - 105 | async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - 106 | await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); - | ^~~~~~~~~~~~~~~~~~~~~ + 105 | async function publicKeyWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + 106 | await crypto.subtle.getPublicKey(key, ["verify"]); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~ 107 | } - hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, wrapKey and unwrapKey with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle fenced.ts:111:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet diff --git a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt index e46099f699..147afb0d32 100644 --- a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt +++ b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt @@ -5,7 +5,7 @@ webcrypto-refusals.ts:6:16 - error SC2020: 'crypto.subtle as a value' is part of | ^~~~~~~~~~~~~ 7 | console.log(typeof subtle); - hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, wrapKey and unwrapKey with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle webcrypto-refusals.ts:9:20 - error SC2020: 'crypto.subtle.sign as a value' is part of the standard library types but has no scriptc lowering yet @@ -14,4 +14,4 @@ webcrypto-refusals.ts:9:20 - error SC2020: 'crypto.subtle.sign as a value' is pa | ^~~~~~~~~~~~~~~~~~ 10 | - hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, wrapKey and unwrapKey with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file diff --git a/tests/harness/corpus-costs/7130-webcrypto-wrap-key.ts.json b/tests/harness/corpus-costs/7130-webcrypto-wrap-key.ts.json new file mode 100644 index 0000000000..56b3458cae --- /dev/null +++ b/tests/harness/corpus-costs/7130-webcrypto-wrap-key.ts.json @@ -0,0 +1 @@ +{ "plain": 5334, "san": 8182 } diff --git a/tests/harness/corpus-costs/7131-webcrypto-wrap-key-errors.mjs.json b/tests/harness/corpus-costs/7131-webcrypto-wrap-key-errors.mjs.json new file mode 100644 index 0000000000..5cb958a530 --- /dev/null +++ b/tests/harness/corpus-costs/7131-webcrypto-wrap-key-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 5838, "san": 8032 } diff --git a/tests/harness/corpus-costs/7132-jose-pbes2/main.mjs.json b/tests/harness/corpus-costs/7132-jose-pbes2/main.mjs.json new file mode 100644 index 0000000000..3ad38d83d8 --- /dev/null +++ b/tests/harness/corpus-costs/7132-jose-pbes2/main.mjs.json @@ -0,0 +1 @@ +{ "plain": 30866, "san": 122276 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 00ee400436..d321b9a002 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1666,6 +1666,7 @@ await attempt("ed25519 key", () => crypto.subtle.generateKey({ name: "Ed25519" } const aes = await crypto.subtle.generateKey({ name: "AES-GCM", length: 128 }, false, ["encrypt"]); await attempt("aes-ocb", () => crypto.subtle.encrypt({ name: "AES-OCB", iv: new Uint8Array(12) }, aes, bytes)); await attempt("ecdh bits", () => crypto.subtle.deriveBits({ name: "ECDH" }, key, 256)); +await attempt("rsa-oaep wrap", () => crypto.subtle.wrapKey("raw", key, key, { name: "RSA-OAEP" })); `, "mjs", ); @@ -1678,23 +1679,24 @@ await attempt("ecdh bits", () => crypto.subtle.deriveBits({ name: "ECDH" }, key, "sha3 hmac Error SC2020 crypto.subtle.importKey with the SHA3-256 algorithm is not supported yet\n" + "ed25519 key Error SC2020 crypto.subtle.generateKey with the Ed25519 algorithm is not supported yet\n" + "aes-ocb Error SC2020 crypto.subtle.encrypt with the AES-OCB algorithm is not supported yet\n" + - "ecdh bits Error SC2020 crypto.subtle.deriveBits with the ECDH algorithm is not supported yet\n", + "ecdh bits Error SC2020 crypto.subtle.deriveBits with the ECDH algorithm is not supported yet\n" + + "rsa-oaep wrap Error SC2020 crypto.subtle.wrapKey with the RSA-OAEP algorithm is not supported yet\n", ); }); test("a method outside the slice is a named fence where it runs", async () => { const r = await compileAndRun( - "webcrypto-wrap-key", + "webcrypto-get-public-key", `console.log("before"); -const key = await crypto.subtle.generateKey({ name: "AES-KW", length: 128 }, true, ["wrapKey"]); -await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); +const key = await crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-256" }, true, ["sign"]); +await crypto.subtle.getPublicKey(key, ["verify"]); `, "mjs", ); expect(r.exitCode).toBe(1); expect(r.stdout).toBe("before\n"); expect(r.stderr).toMatch( - /^Uncaught Error: 'crypto\.subtle\.wrapKey' is part of the standard library types but has no scriptc lowering yet.*\n$/, + /^Uncaught Error: 'crypto\.subtle\.getPublicKey' is part of the standard library types but has no scriptc lowering yet.*\n$/, ); }); }); From 44efd4b55bf1fb0b4500699aa5134fd829ef1c18 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:49:11 -0500 Subject: [PATCH 113/395] 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 fa2e92568c..ed2f7b1765 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 0000000000..2c7b8ea0a3 --- /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 0000000000..6387a0e4b3 --- /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 7e22450daa..35e866d6a4 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 6b0a2fea54..53b22cfaa7 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; + 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 { ...cached.input, llvm: cached.llvm, ir: cached.ir }; + 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 114/395] 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 | 143 +++--- .../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 | 137 ++--- .../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, 1143 insertions(+), 197 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 ae36f736d1..258e55ac82 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 0381803f6a..cc16f74d5d 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 6911ef98b8..c48468886f 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(); + const orders = + shape.accessorOrder !== undefined ? [shape.accessorOrder] : shapeKeyOrders(shape); + const perOrder = (order: readonly string[]): void => { + const inOrder = new Set(order); + const accessors = new Set( + shape.accessorOrder === undefined + ? [] + : shape.fields.flatMap((f) => { + const slot = accessorSlotProp(f.name); + return slot === null ? [] : [slot.prop]; + }), + ); + type DynField = { name: string; type: IrType } | { accessor: string }; + const dynFields: DynField[] = [ + ...order.flatMap((n) => { + if (accessors.has(n)) return [{ accessor: n } as DynField]; + const f = byName.get(n); + return f === undefined ? [] : [f as DynField]; + }), + ...shape.fields.filter( + (f) => + !inOrder.has(f.name) && + f.name !== KEY_ORDER_FIELD && + !(accessors.size > 0 && accessorSlotProp(f.name) !== null), + ), + ]; + for (const entry of dynFields) { + if ("accessor" in entry) { + host.declare( + `declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`, + ); + const boxed = (kind: "get" | "set"): string => { + const slot = byName.get(`%${kind}:${entry.accessor}`); + if (slot === undefined) return "null"; + const fv = loadFieldOf(slot.name, slot.type); + const conv = B.tmp(); + B.line( + `${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`, + ); + return conv; + }; + const getter = boxed("get"); + const setter = boxed("set"); B.line( - `${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`, + `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)}`, ); - return conv; - }; - const getter = boxed("get"); - const setter = boxed("set"); + 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_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`, + `call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`, ); - 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 (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 9dd6bbcac4..af1a9a4863 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 a0a0cf24a6..b642041227 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 0000000000..9dbda14140 --- /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 46dbc86e1c..25e63251e0 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); + } + 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)); } - 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 1c362cb09f..cf3c632d7a 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 de16e5d611..56b65a3d30 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 2fc9a6cbda..c35860d435 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 bd5068d056..0d45628ed2 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 47f490c7ba..57f16bcc7d 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 9abd5a0b0c..ee942c0328 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 7bef40aa68..0a802c03de 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 4a395feb2c..89a5ee73ad 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 0000000000..3551ad6b19 --- /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 0000000000..4451e935a5 --- /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 0000000000..e87101734e --- /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 0000000000..b13a132618 --- /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 0000000000..a9f55663d9 --- /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 0000000000..5a091d157b --- /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 0000000000..407a2179e6 --- /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 0000000000..0cd3dff614 --- /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 0000000000..29f725c24d --- /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 0000000000..89ed16edba --- /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 0000000000..6a11eaf2c7 --- /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 0000000000..daafba23b3 --- /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 0000000000..55970cb40a --- /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 0000000000..b1afd8761c --- /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 0000000000..aa86580075 --- /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 0000000000..58d7b29f85 --- /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 0000000000..e0b1e4dd16 --- /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 0000000000..932ea4809c --- /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 0000000000..ba3e270eed --- /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 0000000000..12680c6af2 --- /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 0000000000..0b945a1c9c --- /dev/null +++ b/tests/harness/corpus-costs/7128-key-order-late-writes.ts.json @@ -0,0 +1 @@ +{ "plain": 1700, "san": 2500 } From c14e2607c55bafdc05e3ff7f961fb8a57778ad20 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:52:31 -0500 Subject: [PATCH 115/395] Address #1785 review: Buffer|string data adapter and objectMode chunk handling --- .../src/backend/llvm/expr-stream-callbacks.ts | 12 ++++++++++++ packages/runtime/src/scr_stream.c | 9 +++++++++ tests/corpus/7241-objectmode-streams.mjs | 7 +++++++ 3 files changed, 28 insertions(+) diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 17507e1280..0570d0ea7b 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -126,6 +126,9 @@ export function streamDataAdapter( host.declare(`declare void @scr_bytes_release_v(ptr)`); host.declare(`declare ptr @scr_str_retain_v(ptr)`); host.declare(`declare void @scr_str_release_v(ptr)`); + host.declare(`declare void @scr_throw_error_msg(i32, ptr, ${host.sizeType})`); + const unionMsg = + "a 'data' listener declaring a Buffer | string chunk received an object-mode value that is neither (declare the chunk's type)"; d.push( ` %uslot = alloca ptr`, ` %isb = icmp ne ptr %a0, null`, @@ -136,6 +139,15 @@ export function streamDataAdapter( ` store ptr %ub, ptr %uslot`, ` br label %go`, `str:`, + // an object-mode value that is neither: the catchable error the + // one-type adapters throw + ` %nos = icmp eq ptr %a1, null`, + ` br i1 %nos, label %bad, label %strok`, + `bad:`, + ` call void @scr_throw_error_msg(i32 1, ptr ${host.cstr(unionMsg)}, ${host.sizeType} ${Buffer.byteLength(unionMsg, "utf8")})`, + ` call void @scr_closure_release(ptr %orig)`, + ` ret void`, + `strok:`, ` %rs = call ptr @scr_str_retain_v(ptr %a1)`, ` %us = call ptr @scr_union_new_ref(i32 ${strTag}, ptr %rs, ptr @scr_str_retain_v, ptr @scr_str_release_v, ptr null)`, ` store ptr %us, ptr %uslot`, diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 0c17c8840d..be5ccb9107 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -5637,6 +5637,8 @@ bool scr_stream_push_str(ScrStream *s, ScrStr *str) { /* push(chunk, encoding) with an explicit non-utf8 literal: the per-call * encoding overrides the stream's default. Borrows both strings. */ bool scr_stream_push_str_enc(ScrStream *s, ScrStr *str, ScrStr *enc) { + /* object mode: the string is the chunk and the encoding is not used */ + if (s->st->has_r && s->st->r.object_entries) return scr_stream_push_str(s, str); ScrBytes *b = scr_stream_buffer(scr_bytes_from_str(str, enc)); bool ret = scr_stream_add_chunk(s, b, false); scr_bytes_release(b); @@ -5702,6 +5704,13 @@ void scr_stream_unshift(ScrStream *s, ScrBytes *chunk) { } void scr_stream_unshift_str(ScrStream *s, ScrStr *str) { + if (s->st->has_r && s->st->r.object_entries) { + /* object mode: the string itself goes to the front */ + ScrDyn *v = scr_dyn_new_str(str); + scr_stream_unshift_dyn(s, v); + scr_dyn_release(v); + return; + } 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); diff --git a/tests/corpus/7241-objectmode-streams.mjs b/tests/corpus/7241-objectmode-streams.mjs index 334273c002..6101930dff 100644 --- a/tests/corpus/7241-objectmode-streams.mjs +++ b/tests/corpus/7241-objectmode-streams.mjs @@ -100,6 +100,13 @@ function unshiftFront() { r.unshift({ id: 0 }); r.on("data", (v) => console.log("after unshift:", show(v))); r.on("end", () => console.log("read end")); + // strings stay strings on an object-mode side, whatever the encoding + const strings = new Readable({ objectMode: true, read() {} }); + strings.push("b"); + strings.unshift("a"); + strings.push("6869", "hex"); + strings.push(null); + strings.on("data", (v) => console.log("string entry:", show(v))); } function piped() { From ce47b9bad95fd3284f151cd5c04d0ecc9dea881a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:54:53 -0500 Subject: [PATCH 116/395] 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 3551ad6b19..0000000000 --- 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 4451e935a5..0000000000 --- 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 e87101734e..0000000000 --- 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 b13a132618..0000000000 --- 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 a9f55663d9..0000000000 --- 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 5a091d157b..0000000000 --- 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 407a2179e6..0000000000 --- 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 0000000000..2609ff9290 --- /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 0000000000..40729240a0 --- /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 0000000000..e44503626d --- /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 0000000000..431bf5c9cf --- /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 0000000000..fea64a145c --- /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 0000000000..3b7cab36fa --- /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 0000000000..7f5302cd53 --- /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 3cefd421a132f1d1502e45405bf7e11981fb4286 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 05:58:32 -0500 Subject: [PATCH 117/395] Fence requestCert beside a secureContext and free failed TLSSockets new tls.TLSSocket(socket, { isServer: true, secureContext, requestCert: true }) skipped the options walk that refuses requestCert, so the server asked for no client certificate and accepted every client, even with rejectUnauthorized: true. With a secureContext, Node still reads the socket's own options, so a truthy requestCert, SNICallback and ALPNCallback now throw the fence there too. A TLS socket's transport kept its 'secure' and 'session' listeners until the socket was freed. A handshake that fails never fires 'secure', so a listener that captures the socket kept the socket and its TLS state alive after it closed. The transport now drops those listeners when the socket settles, and ignores listeners added after that. --- .../7094-tls-socket-failed-handshakes.ts.json | 6 ++ packages/runtime/src/scr_net.c | 1 + packages/runtime/src/scr_net.h | 3 + packages/runtime/src/scr_tls.c | 49 ++++++++++++- .../7094-tls-socket-failed-handshakes.ts | 49 +++++++++++++ .../7094-tls-socket-failed-handshakes.ts.json | 1 + tests/harness/errors.test.ts | 45 ++++++++++++ tests/harness/tls-socket-leaks.test.ts | 68 +++++++++++++++++++ 8 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7094-tls-socket-failed-handshakes.ts.json create mode 100644 tests/corpus/7094-tls-socket-failed-handshakes.ts create mode 100644 tests/harness/corpus-costs/7094-tls-socket-failed-handshakes.ts.json create mode 100644 tests/harness/tls-socket-leaks.test.ts diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7094-tls-socket-failed-handshakes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7094-tls-socket-failed-handshakes.ts.json new file mode 100644 index 0000000000..8fa5241553 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7094-tls-socket-failed-handshakes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7094-tls-socket-failed-handshakes.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 8f9a684248..29a4af3936 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -5354,6 +5354,7 @@ static bool scr_net_sock_settle(ScrNetSocket *sock) { scr_net_ls_drop(&sock->wcb_ls); scr_net_ls_drop(&sock->ready_ls); scr_net_ls_drop(&sock->lookup_ls); + if (sock->tops && sock->tops->settle) sock->tops->settle(sock->tctx); if (sock->pipe_dst) { scr_net_sock_release(sock->pipe_dst); sock->pipe_dst = NULL; diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index 8f26bbf4d5..ffa9b2f72f 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -354,6 +354,9 @@ typedef struct ScrNetTransportOps { /* nullable: runs after the established socket's 'connect' listeners * (the TLS 'secure' event, which Node emits after secureConnect) */ void (*after_established)(void *tctx); + /* nullable: runs when the closed socket settles, after its own + * listeners drop; the transport drops the listeners it holds */ + void (*settle)(void *tctx); } ScrNetTransportOps; void scr_net_sock_set_transport(ScrNetSocket *s, const ScrNetTransportOps *ops, void *tctx); /* new tls.TLSSocket(socket) and tls.connect({ socket }): a fresh socket diff --git a/packages/runtime/src/scr_tls.c b/packages/runtime/src/scr_tls.c index 6fa1f169d7..e7436ab695 100644 --- a/packages/runtime/src/scr_tls.c +++ b/packages/runtime/src/scr_tls.c @@ -664,6 +664,7 @@ typedef struct ScrTlsSock { size_t head_len; /* the 'secure' event's listeners */ ScrNetLs secure_ls; + bool settled; /* the socket closed: listeners added later never fire */ } ScrTlsSock; /* checkServerIdentity for an IP literal: the address must be one of the @@ -1237,6 +1238,17 @@ static void scr_tls_after_established(void *tctx) { scr_net_ls_drop(&t->secure_ls); } +/* The closed socket settles: its 'secure' and 'session' listeners can no + * longer fire, and a listener that captures the socket would otherwise + * keep it, and this transport, alive (a handshake that fails never fires + * 'secure'). */ +static void scr_tls_settle(void *tctx) { + ScrTlsSock *t = (ScrTlsSock *)tctx; + t->settled = true; + scr_net_ls_drop(&t->secure_ls); + scr_net_ls_drop(&t->session_ls); +} + static const ScrNetTransportOps SCR_TLS_OPS = { .xread = scr_tls_xread, .xwrite = scr_tls_xwrite, @@ -1245,6 +1257,7 @@ static const ScrNetTransportOps SCR_TLS_OPS = { .pending = scr_tls_pending, .shutdown_write = scr_tls_shutdown_write, .free = scr_tls_free, + .settle = scr_tls_settle, .after_established = scr_tls_after_established, }; @@ -2202,6 +2215,29 @@ static const char *const SCR_TLS_WRAP_FENCED_OPTIONS[] = { "server", "session", "requestOCSP", "pskCallback", "enableTrace", NULL, }; +/* The socket-level options of a wrap that serves a secureContext, which + * the server-options walk would otherwise check: requestCert (client + * certificates are not modeled), SNICallback and ALPNCallback fence. + * False with the exception pending. */ +static bool scr_tls_wrap_socket_opts_check(const ScrDyn *record, const char *api) { + for (size_t i = 0; record != NULL && i < record->v.obj.len; i++) { + const ScrDynEntry *e = &record->v.obj.entries[i]; + if (e->value->kind == SCR_DYN_UNDEF) continue; + if (strcmp(e->key, "requestCert") == 0) { + if (!scr_tls_dyn_truthy(e->value)) continue; + scr_tls_opt_fence(api, "requestCert", + "client-certificate handshakes are not modeled (SEMANTICS.md divergence 55) — requestCert stays at Node's false default"); + return false; + } + if (strcmp(e->key, "SNICallback") == 0 || strcmp(e->key, "ALPNCallback") == 0) { + scr_tls_opt_fence(api, e->key, + "isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members"); + return false; + } + } + return true; +} + ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrowed*/, ScrSecureCtx *ctx /*borrowed, nullable*/) { static const char api[] = "new tls.TLSSocket"; @@ -2233,12 +2269,17 @@ ScrNetSocket *scr_tls_wrap_socket(ScrNetSocket *raw, const ScrDyn *opts /*borrow return NULL; } /* Node serves a secureContext as it is: the record's cert, key and - * other context options are not read then */ + * other context options are not read then. The socket's own options + * still apply, so the ones without a lowering fence either way: + * requestCert asks for a client certificate this runtime cannot + * verify. */ int min_version = 12, max_version = 13; ScrBytes *cert = NULL, *key = NULL; if (ctx == NULL) { if (!scr_tls_versions_dyn(record, &min_version, &max_version)) return NULL; if (!scr_tls_srv_opts_walk_ex(record, api, &cert, &key, true)) return NULL; + } else if (!scr_tls_wrap_socket_opts_check(record, api)) { + return NULL; } char **alpn = NULL; const ScrDyn *protocols = record != NULL ? scr_dyn_obj_get(record, "ALPNProtocols", 13) : NULL; @@ -2867,7 +2908,7 @@ void scr_tls_sock_on_secure_connect(ScrNetSocket *s, ScrClosure *cb /*moves*/, b * handshake comes too late, as in Node. */ void scr_tls_sock_on_secure(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) { ScrTlsSock *t = scr_tls_sock_of(s); - if (t == NULL || t->established) { + if (t == NULL || t->established || t->settled) { scr_closure_release(cb); return; } @@ -2880,6 +2921,10 @@ void scr_tls_sock_on_session(ScrNetSocket *s, ScrClosure *cb /*moves*/, void *fn scr_closure_release(cb); /* a plain socket never fires it, like Node */ return; } + if (t->settled) { + scr_closure_release(cb); + return; + } scr_net_ls_add(&t->session_ls, cb, fn, once); if (t->established) scr_tls_fire_session(t); /* the ticket already arrived */ } diff --git a/tests/corpus/7094-tls-socket-failed-handshakes.ts b/tests/corpus/7094-tls-socket-failed-handshakes.ts new file mode 100644 index 0000000000..daad49a4ed --- /dev/null +++ b/tests/corpus/7094-tls-socket-failed-handshakes.ts @@ -0,0 +1,49 @@ +// A TLSSocket whose handshake never completes still closes and releases +// its listeners, including a 'secure' listener that holds the socket: one +// peer closes before sending anything and the next sends a plain HTTP +// request. +import { readFileSync } from "node:fs"; +import * as net from "node:net"; +import * as tls from "node:tls"; + +const certs = "tests/fixtures/server/certs"; +const cert = readFileSync(`${certs}/localhost.pem`); +const key = readFileSync(`${certs}/localhost-key.pem`); +const context = tls.createSecureContext({ cert, key }); + +const peers: ((port: number) => void)[] = [ + (port) => { + const quiet = net.connect(port, "127.0.0.1", () => quiet.end()); + }, + (port) => { + const plain = net.connect(port, "127.0.0.1", () => plain.write("GET / HTTP/1.1\r\nHost: x\r\n\r\n")); + plain.on("error", () => {}); + plain.resume(); + }, + (port) => { + const quiet = net.connect(port, "127.0.0.1", () => quiet.destroy()); + }, +]; +let served = 0; +const server = net.createServer((raw) => { + served++; + const secure = + served === 3 + ? new tls.TLSSocket(raw, { isServer: true, secureContext: context }) + : new tls.TLSSocket(raw, { isServer: true, cert, key }); + secure.on("secure", () => console.log("secure", served, secure.alpnProtocol)); + secure.on("error", (err: NodeJS.ErrnoException) => console.log("error", served, err.code)); + secure.on("close", (hadError: boolean) => { + console.log("close", served, hadError); + next(); + }); +}); +function next(): void { + const peer = peers.shift(); + if (peer === undefined) { + server.close(() => console.log("server closed")); + return; + } + peer((server.address() as net.AddressInfo).port); +} +server.listen(0, "127.0.0.1", next); diff --git a/tests/harness/corpus-costs/7094-tls-socket-failed-handshakes.ts.json b/tests/harness/corpus-costs/7094-tls-socket-failed-handshakes.ts.json new file mode 100644 index 0000000000..ec5193be75 --- /dev/null +++ b/tests/harness/corpus-costs/7094-tls-socket-failed-handshakes.ts.json @@ -0,0 +1 @@ +{ "plain": 2618, "san": 3905 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 96949201dc..31f2b6fd9b 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -905,6 +905,51 @@ listener.listen(0, () => { }); }); +// With a secureContext, the TLSSocket's own options still apply: a truthy +// requestCert, which asks for client certificates this runtime does not +// verify, and SNICallback throw the fence instead of being dropped. +describe("tls.TLSSocket socket options beside a secureContext", () => { + test("requestCert and SNICallback throw the fence", async () => { + const r = await compileAndRun( + "tls-socket-context-fences", + `import { readFileSync } from "node:fs"; +import * as net from "node:net"; +import * as tls from "node:tls"; +const certs = "tests/fixtures/server/certs"; +const cert = readFileSync(\`\${certs}/localhost.pem\`); +const key = readFileSync(\`\${certs}/localhost-key.pem\`); +const context = tls.createSecureContext({ cert, key }); +function report(label: string, run: () => void): void { + try { + run(); + console.log(label, "no error"); + } catch (error) { + console.log(label, (error as Error).message); + } +} +const listener = net.createServer((raw) => { + report("requestCert", () => new tls.TLSSocket(raw, { isServer: true, secureContext: context, requestCert: true, rejectUnauthorized: true })); + report("SNICallback", () => new tls.TLSSocket(raw, { isServer: true, secureContext: context, SNICallback: (name: string, cb: (err: Error | null, ctx?: tls.SecureContext) => void) => cb(null, context) })); + report("requestCert false", () => new tls.TLSSocket(raw, { isServer: true, secureContext: context, requestCert: false, rejectUnauthorized: true }).destroy()); + listener.close(); +}); +listener.listen(0, () => { + const c = net.connect((listener.address() as net.AddressInfo).port, "localhost"); + c.on("error", () => {}); + c.on("close", () => console.log("client closed")); +}); +`, + ); + expect(r.stderr).toBe(""); + expect(r.stdout).toBe( + "requestCert new tls.TLSSocket option 'requestCert' has no static lowering — client-certificate handshakes are not modeled (SEMANTICS.md divergence 55) — requestCert stays at Node's false default\n" + + "SNICallback new tls.TLSSocket option 'SNICallback' has no static lowering — isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members\n" + + "requestCert false no error\n" + + "client closed\n", + ); + }); +}); + // A curve Node lists in crypto.getCurves() but the runtime does not // implement throws a coded SC2020 error naming the curve when the name is // computed; literal names are refused at compile time (node-types-fenced). diff --git a/tests/harness/tls-socket-leaks.test.ts b/tests/harness/tls-socket-leaks.test.ts new file mode 100644 index 0000000000..64edfac252 --- /dev/null +++ b/tests/harness/tls-socket-leaks.test.ts @@ -0,0 +1,68 @@ +/* new tls.TLSSocket under LeakSanitizer. A TLS socket's transport holds + * its 'secure' and 'session' listeners, which often capture the socket; + * every socket that closes, including one whose handshake fails, must drop + * them. The sanitized lanes otherwise run with detect_leaks=0, so this runs + * the TLSSocket programs with LeakSanitizer explicitly on. 7094's + * handshakes all fail. Linux only (Apple's ASan has no LeakSanitizer); + * runs in the SCRIPTC_SAN=1 lane. */ +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { describe, expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +const programs = [ + "tests/corpus/7087-tls-socket-wrap.ts", + "tests/corpus/7088-tls-socket-connect-tunnel.ts", + "tests/corpus/7094-tls-socket-failed-handshakes.ts", +]; + +describe.skipIf(!sanitize || process.platform !== "linux")( + "tls.TLSSocket under LeakSanitizer", + () => { + test.each(programs)("%s reports no leaks", { timeout: 300_000 }, async (program) => { + const file = join(repoRoot, program); + const key = createHash("sha256") + .update(file) + .update(readFileSync(file)) + .update("san") + .digest("hex") + .slice(0, 16); + const outDir = join(cacheDir, `tls-socket-leaks-${key}`); + mkdirSync(outDir, { recursive: true }); + const result = await compile(file, { + outPath: join(outDir, "program"), + outDir, + sanitize: true, + backend: "llvm", + }); + if (!result.ok) { + throw new Error( + "program failed to compile:\n" + + result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"), + ); + } + const run = await execFileAsync(result.binaryPath, [], { + env: { ...process.env, ASAN_OPTIONS: "detect_leaks=1" }, + maxBuffer: 16 * 1024 * 1024, + }).catch((error: { code?: number; stdout?: string; stderr?: string }) => error); + expect(run.stderr).toBe(""); + expect("code" in run ? run.code : 0).toBe(0); + const expected = await execFileAsync( + process.execPath, + ["--no-warnings", "--experimental-strip-types", file], + { + maxBuffer: 16 * 1024 * 1024, + }, + ); + expect(run.stdout).toBe(expected.stdout); + }); + }, +); From 4e834f8eed49b694be5fb5c1b14ba4f827342a68 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 06:00:28 -0500 Subject: [PATCH 118/395] Read error constructors as values and destructure workerData in JavaScript (reland) --- .../lowering/expressions/object-literals.ts | 25 +++++- .../lowering/expressions/property-access.ts | 34 +++++++- .../src/frontend/lowering/lower-calls.ts | 29 +++++++ .../src/frontend/lowering/lower-stmts.ts | 3 + .../6870-error-constructor-values.js.json | 6 ++ .../6871-worker-data-destructuring.cjs.json | 6 ++ packages/runtime/src/scr_dyn.h | 3 + packages/runtime/src/scr_dyn_invoke.c | 2 + packages/runtime/src/scr_json.c | 79 ++++++++++++++++--- tests/corpus/6870-error-constructor-values.js | 65 +++++++++++++++ .../corpus/6871-worker-data-destructuring.cjs | 30 +++++++ .../6870-error-constructor-values.js.json | 1 + .../6871-worker-data-destructuring.cjs.json | 1 + 13 files changed, 267 insertions(+), 17 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json create mode 100644 tests/corpus/6870-error-constructor-values.js create mode 100644 tests/corpus/6871-worker-data-destructuring.cjs create mode 100644 tests/harness/corpus-costs/6870-error-constructor-values.js.json create mode 100644 tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 015be79148..678b375fee 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -36,6 +36,7 @@ import { tryLowerExpression } from "./try-lower-expression.js"; import { fenceSymbolFieldCopy } from "../symbol-fields.js"; import { planUnionRetag } from "../union-retag.js"; import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js"; +import { builtinGlobalValue } from "./property-access.js"; /** `{ a: 1, b: "x" }` → recordLit. The record type comes from the * contextual type when tsc has one (annotated declarations, arguments, @@ -394,9 +395,11 @@ export function lowerDynObjectLiteral( const lowerValue = (): IrExpr => ts.isMethodDeclaration(prop) ? lowerer.lowerLambda(prop, true) - : !boxValue && ts.isObjectLiteralExpression(valueExpr) - ? lowerer.lowerExprExpecting(valueExpr, DYN) - : lowerer.lowerExpr(valueExpr as ts.Expression); + : ts.isShorthandPropertyAssignment(prop) && shorthandBuiltinValue(lowerer, prop) + ? shorthandBuiltinValue(lowerer, prop)! + : !boxValue && ts.isObjectLiteralExpression(valueExpr) + ? lowerer.lowerExprExpecting(valueExpr, DYN) + : lowerer.lowerExpr(valueExpr as ts.Expression); raw = fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ?? lowerValue(); } catch (err) { // A PURE member read a JS file cannot lower (a namespace object @@ -3157,6 +3160,20 @@ function lowerDeclaredSpreadMerge( /** The value of a shorthand property (`{ x }`): the binding `x` refers to. * The property name's own symbol is the PROPERTY, so resolution goes * through getShorthandAssignmentValueSymbol. */ +/** `{ TypeError }`: a stdlib global in shorthand position reads as the value + * its name reads as elsewhere (the interned constructor or function value), + * or null when the global has no such value. The shorthand's name carries + * the property's symbol, so the identifier path cannot resolve it. */ +function shorthandBuiltinValue( + lowerer: Lowerer, + prop: ts.ShorthandPropertyAssignment, +): IrExpr | null { + const symbol = lowerer.checker.getShorthandAssignmentValueSymbol(prop); + if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; + const js = isJsSourceFile(ts.getSourceFileOfNode(prop)); + return builtinGlobalValue(ts.symbolName(symbol), locOf(prop.name), js); +} + export function lowerShorthandValue( lowerer: Lowerer, prop: ts.ShorthandPropertyAssignment, @@ -3173,6 +3190,8 @@ export function lowerShorthandValue( const callable = lowerer.lowerBuiltinCallableValue(builtin, loc); if (callable) return callable; } + const builtinValue = shorthandBuiltinValue(lowerer, prop); + if (builtinValue) return builtinValue; if (symbol) { if (lowerer.ctx.selfSymbol === symbol) { return { kind: "selfRef", type: lowerer.ctx.selfType!, loc }; diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 957dd1ddfa..7f3910c158 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -4090,10 +4090,26 @@ function lowerStreamObjectProperty( return lowerStreamProperty(lowerer, expr, info); } +/** The error constructors among GLOBAL_CONSTRUCTOR_VALUES. Their statics + * (`Error.stackTraceLimit`, `Error.captureStackTrace`) keep their own + * lowerings, so fenceBuiltinConstructorStaticRead leaves them alone. */ +const ERROR_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ + "Error", + "TypeError", + "RangeError", + "SyntaxError", + "ReferenceError", + "EvalError", + "URIError", + "AggregateError", +]); + /** The builtin constructors JavaScript sources read as real function * values rather than identity tokens: the runtime interns one function per * constructor, and dynamic values answer `.constructor` with the same one, - * so `o.constructor === Object` is an exact identity test. */ + * so `o.constructor === Object` and `err.constructor === TypeError` are + * exact identity tests, and `err.constructor.name` is the constructor's + * name. */ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ "Object", "Function", @@ -4104,6 +4120,7 @@ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = new Set([ "Promise", "WeakMap", "WeakSet", + ...ERROR_CONSTRUCTOR_VALUES, ]); /** The builtin global functions JavaScript sources read as interned @@ -4154,8 +4171,18 @@ export function builtinGlobalValue(canonical: string, loc: SrcLoc, js = false): const BUILTIN_VALUE_MEMBERS: ReadonlySet = new Set(["name", "length", "constructor"]); /** The Object statics whose answers for an interned builtin function value - * match Node's (identity, and the empty enumerable own-key lists). */ -const BUILTIN_VALUE_REFLECTION: ReadonlySet = new Set(["is", "keys", "values", "entries"]); + * match Node's (identity, the empty enumerable own-key lists, and + * extensibility: freezing, sealing and preventExtensions are fenced, so the + * values stay extensible). */ +const BUILTIN_VALUE_REFLECTION: ReadonlySet = new Set([ + "is", + "keys", + "values", + "entries", + "isExtensible", + "isFrozen", + "isSealed", +]); /** The value `builtinGlobalValue` gives the stdlib global `expr` read at its * use site: null when the use reads the global's members (they keep their @@ -4246,6 +4273,7 @@ function fenceBuiltinConstructorStaticRead( symbol !== undefined && ts.symbolName(symbol).endsWith("Constructor") && GLOBAL_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) && + !ERROR_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) && lowerer.isStdlibSymbol(symbol) ) lowerer.stdlibMemberFence(access); diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index bd5068d056..39b7207c03 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -7439,6 +7439,35 @@ function lowerObjectStaticCall( if (call.questionDotToken || access.questionDotToken) return null; if (!lowerer.isStdlibGlobal(access.expression, "Object")) return null; const member = access.name.text; + // `Object.getOwnPropertyDescriptor(Error, "stackTraceLimit")` (Effect's + // isStackTraceLimitWritable): Error's own writable, enumerable, + // configurable data property holding the current limit. The interned + // Error value models no other statics (builtinGlobalValueAt). + const [first, second] = call.arguments.nodes; + if ( + member === "getOwnPropertyDescriptor" && + call.arguments.nodes.length === 2 && + lowerer.isStdlibGlobal(first!, "Error") && + ts.isStringLiteralLike(second!) && + second.text === "stackTraceLimit" + ) { + const loc = locOf(call); + const dyn = (value: IrExpr): IrExpr => ({ kind: "dynFrom", value, type: DYN, loc }); + const yes = dyn({ kind: "boolLit", value: true, type: BOOL, loc }); + const limit: IrExpr = { kind: "libCall", fn: "error.stackLimitGet", args: [], type: F64, loc }; + return { + kind: "dynObjLit", + fields: [ + { key: { kind: "strLit", value: "value", type: STRING, loc }, value: dyn(limit) }, + ...["writable", "enumerable", "configurable"].map((key) => ({ + key: { kind: "strLit" as const, value: key, type: STRING, loc }, + value: yes, + })), + ], + type: DYN, + loc, + }; + } if ( (member === "getOwnPropertyNames" || member === "getOwnPropertySymbols" || diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 687420baf3..7c0b4f7ff8 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -3889,6 +3889,9 @@ function stdlibGlobalTokenDestructure( if (decl.initializer === undefined || !isJsSourceFile(ts.getSourceFileOfNode(decl))) return null; const globalName = stdlibGlobalNameOf(lowerer, decl.initializer); if (globalName === null) return null; + // `workerData` is a value (the data the worker was started with), not an + // API surface: its members destructure as any untyped value's do. + if (globalName === "workerData") return null; if (!ts.isObjectBindingPattern(decl.name)) { lowerer.unsupported( "SC1031", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json new file mode 100644 index 0000000000..a2f0e02ea5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6870-error-constructor-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6870-error-constructor-values.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json new file mode 100644 index 0000000000..b610a58788 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6871-worker-data-destructuring.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6871-worker-data-destructuring.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 9220dd8439..facf9f2fff 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -764,6 +764,9 @@ ScrDyn *scr_dyn_array_constructor(void); /* The interned builtin constructor `name` (Object, Date, RegExp, Map, Set, * Promise, WeakMap, WeakSet) as a dynamic function value: +1. */ ScrDyn *scr_dyn_global_constructor(const ScrStr *name); +/* The interned function value of an error constructor ("TypeError"), or + * NULL for other names. */ +ScrDyn *scr_dyn_error_constructor(const char *name); /* A builtin global function (setTimeout, setImmediate, process.nextTick, ...) * as an interned dynamic function value: +1. */ ScrDyn *scr_dyn_builtin_function(const ScrStr *name); diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index ec49dd3831..80e01b08c5 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -1923,6 +1923,8 @@ static ScrDyn *dyn_function_constructor(void) { ScrDyn *scr_dyn_global_constructor(const ScrStr *name) { if (strcmp(name->data, dyn_s20) == 0) return dyn_function_constructor(); + ScrDyn *error = scr_dyn_error_constructor(name->data); + if (error) return error; ScrDyn *ctor = dyn_global_ctor(name->data); if (!ctor) scr_trap(dyn_s3); return ctor; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 4e21fa397c..f610ffdbb8 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1814,6 +1814,55 @@ static void scr_builtin_object_prototype_cleanup(void) { scr_dyn_release(prototype); } +/* The error constructors as values (an error's `constructor`, and the + * globals in JavaScript): one interned function per constructor and thread, + * indexed like scr_error_vts, with the constructor's name and length. + * Calling one through the value is not supported yet, as for the other + * interned constructors (scr_dyn_global_constructor). */ +static const char *const scr_error_ctor_names[] = { + "Error", "TypeError", "RangeError", "SyntaxError", NULL, + "ReferenceError", "EvalError", "URIError", "AggregateError", +}; +#define SCR_ERROR_CTORS (sizeof scr_error_ctor_names / sizeof scr_error_ctor_names[0]) +static SCR_TL ScrDyn *scr_error_ctor_values[SCR_ERROR_CTORS]; +static SCR_TL bool scr_error_ctors_registered; + +static void scr_error_ctors_cleanup(void) { + for (size_t i = 0; i < SCR_ERROR_CTORS; i++) { + scr_dyn_release(scr_error_ctor_values[i]); + scr_error_ctor_values[i] = NULL; + } +} + +static ScrDyn *scr_error_ctor_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + char message[128]; + int length = snprintf(message, sizeof message, + "calling the %s constructor through a stored value is not supported yet", + (const char *)closure->fn); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, "SC2020"); + return NULL; +} + +ScrDyn *scr_dyn_error_constructor(const char *name) { + for (size_t i = 0; i < SCR_ERROR_CTORS; i++) { + if (!scr_error_ctor_names[i] || strcmp(scr_error_ctor_names[i], name) != 0) continue; + if (!scr_error_ctors_registered) { + scr_error_ctors_registered = true; + scr_atexit(scr_error_ctors_cleanup); + } + if (!scr_error_ctor_values[i]) { + uint32_t length = strcmp(name, "AggregateError") == 0 ? 2 : 1; + scr_error_ctor_values[i] = + scr_dyn_new_func(scr_closure_new((void *)scr_error_ctor_names[i], 0), scr_error_ctor_call, length, + "native:constructor", scr_error_ctor_names[i]); + } + return scr_dyn_retain(scr_error_ctor_values[i]); + } + return NULL; +} + ScrDyn *scr_dyn_object_prototype(void) { if (!scr_builtin_object_prototype) { scr_builtin_object_prototype = scr_dyn_alloc(SCR_DYN_OBJ); @@ -7951,6 +8000,17 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { return out; } case SCR_DYN_FUNC: { + /* The interned builtin constructors print with their names, as in Node + * (function Map() { [native code] }); other functions without a source + * text print the anonymous native form. */ + if (d->v.fn.sig && strcmp(d->v.fn.sig, "native:constructor") == 0 && d->v.fn.name) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "function "); + scr_jb_puts(&b, d->v.fn.name); + scr_jb_puts(&b, "() { [native code] }"); + return scr_jb_finish(&b); + } static const char f[] = "function () { [native code] }"; return scr_str_new(f, sizeof f - 1); } @@ -11943,21 +12003,18 @@ static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_ ScrDyn *own = scr_dyn_obj_get(d, key, key_len); if (own) return scr_dyn_retain(own); if (key_len == 11 && memcmp(key, "constructor", 11) == 0) { - static const char *const tokens[] = { - "[builtin Error]", "[builtin TypeError]", - "[builtin RangeError]", "[builtin SyntaxError]", - NULL, "[builtin ReferenceError]", "[builtin EvalError]", "[builtin URIError]", - "[builtin AggregateError]", + /* An error's constructor: the interned function value its global + * reads as (scr_dyn_error_constructor). */ + static const char *const names[] = { + "Error", "TypeError", "RangeError", "SyntaxError", NULL, + "ReferenceError", "EvalError", "URIError", "AggregateError", }; size_t hit = scr_errdyn_find(d, false); const ScrVt *vt = hit == SIZE_MAX ? NULL : scr_errdyn_cache[hit].err->vt; - for (size_t kind = 0; vt && kind < sizeof tokens / sizeof tokens[0]; kind++) { - if (!tokens[kind]) continue; + for (size_t kind = 0; vt && kind < sizeof names / sizeof names[0]; kind++) { + if (!names[kind]) continue; if (vt != &scr_error_vts[kind]) continue; - ScrStr *token = scr_str_new(tokens[kind], strlen(tokens[kind])); - ScrDyn *result = scr_dyn_new_str(token); - scr_str_release(token); - return result; + return scr_dyn_error_constructor(names[kind]); } } return scr_dyn_retain(scr_dyn_undefined()); diff --git a/tests/corpus/6870-error-constructor-values.js b/tests/corpus/6870-error-constructor-values.js new file mode 100644 index 0000000000..79c970914d --- /dev/null +++ b/tests/corpus/6870-error-constructor-values.js @@ -0,0 +1,65 @@ +// The error constructors as values in JavaScript: an error's `constructor` +// is the same function the global names, whose name, length, typeof and +// String() are Node's, for errors the program throws, errors the runtime +// throws, and errors read back from catch bindings and containers. +function describe(label, error) { + const ctor = error.constructor; + console.log(label, error.name, error.constructor.name, typeof ctor, ctor.length, String(ctor)); +} + +describe('Error', new Error('e')); +describe('TypeError', new TypeError('t')); +describe('RangeError', new RangeError('r')); +describe('SyntaxError', new SyntaxError('s')); +describe('ReferenceError', new ReferenceError('f')); +describe('EvalError', new EvalError('v')); +describe('URIError', new URIError('u')); +describe('AggregateError', new AggregateError([], 'a')); + +const thrown = [ + () => JSON.parse('{'), + () => { + const holder = JSON.parse('{"value":null}'); + return holder.value.missing; + }, + () => new Array(-1), + () => decodeURIComponent('%'), + () => { + throw new TypeError('own'); + }, +]; +const constructors = { SyntaxError, TypeError, RangeError, URIError }; +const caught = []; +for (const run of thrown) { + try { + run(); + } catch (error) { + caught.push(error); + console.log('caught', error.constructor.name, error.constructor === constructors[error.name]); + } +} + +const byName = new Map(); +for (const error of caught) byName.set(error.constructor.name, (byName.get(error.constructor.name) ?? 0) + 1); +console.log('kinds', JSON.stringify([...byName])); + +console.log('identity', new TypeError('x').constructor === TypeError, new RangeError('x').constructor === TypeError); +console.log('globals', Error.name, TypeError.length, AggregateError.length, typeof RangeError, String(SyntaxError)); + +const renamed = new TypeError('x'); +renamed.name = 'Custom'; +console.log('renamed', renamed.name, renamed.constructor.name); + +class ParseFailure extends SyntaxError {} +const failure = new ParseFailure('p'); +console.log('subclass', failure.name, failure.constructor.name, failure instanceof SyntaxError); + +// Effect's isStackTraceLimitWritable reflects on Error as a value. +const limit = Object.getOwnPropertyDescriptor(Error, 'stackTraceLimit'); +console.log('stackTraceLimit', JSON.stringify(limit), Object.hasOwn(limit, 'writable') ? limit.writable === true : limit.set !== undefined); +Error.stackTraceLimit = 4; +console.log('stackTraceLimit now', Object.getOwnPropertyDescriptor(Error, 'stackTraceLimit').value); +Error.stackTraceLimit = 10; +for (const ctor of [Error, TypeError, AggregateError, Object, Map]) { + console.log('extensible', ctor.name, Object.isExtensible(ctor), Object.isFrozen(ctor), Object.isSealed(ctor)); +} diff --git a/tests/corpus/6871-worker-data-destructuring.cjs b/tests/corpus/6871-worker-data-destructuring.cjs new file mode 100644 index 0000000000..63ae789bdf --- /dev/null +++ b/tests/corpus/6871-worker-data-destructuring.cjs @@ -0,0 +1,30 @@ +// Destructuring the members of a worker's workerData in JavaScript: plain, +// renamed, defaulted, nested and rest bindings read the data the worker was +// started with, as reading `workerData.` does. +'use strict'; +const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads'); + +if (isMainThread) { + const sab = new SharedArrayBuffer(8); + const data = { name: 'job-7', size: 3, options: { verbose: true, tags: ['a', 'b'] }, counters: new Int32Array(sab), extra: 1, more: 'x' }; + const worker = new Worker(__filename, { workerData: data }); + worker.on('message', (lines) => { + for (const line of lines) console.log(line); + console.log('main sees', new Int32Array(sab)[0]); + }); +} else { + const lines = []; + const { name, size } = workerData; + lines.push(`plain ${name} ${size}`); + const { name: jobName, missing = 'fallback', size: count = 10 } = workerData; + lines.push(`renamed ${jobName} ${missing} ${count}`); + const { + options: { verbose, tags: [firstTag, ...otherTags] }, + } = workerData; + lines.push(`nested ${verbose} ${firstTag} ${JSON.stringify(otherTags)}`); + const { name: ignored, size: alsoIgnored, options, counters, ...rest } = workerData; + lines.push(`rest ${JSON.stringify(rest)} ${typeof options} ${ignored === name && alsoIgnored === size}`); + counters[0] = 41; + counters[0] += 1; + parentPort.postMessage(lines); +} diff --git a/tests/harness/corpus-costs/6870-error-constructor-values.js.json b/tests/harness/corpus-costs/6870-error-constructor-values.js.json new file mode 100644 index 0000000000..c608ce33a4 --- /dev/null +++ b/tests/harness/corpus-costs/6870-error-constructor-values.js.json @@ -0,0 +1 @@ +{ "plain": 1200, "san": 3400 } diff --git a/tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json b/tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json new file mode 100644 index 0000000000..d6c0cb9eea --- /dev/null +++ b/tests/harness/corpus-costs/6871-worker-data-destructuring.cjs.json @@ -0,0 +1 @@ +{ "plain": 1300, "san": 3490 } From fb403d5755dfb737815419cd02960755ab4018a4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 06:06:45 -0500 Subject: [PATCH 119/395] Clear a store that an async prefix enters in a loop callback When an event-loop callback calls an async function or steps a generator that enters a store before its first await, the store becomes the callback's at the end of the prefix. On the main slot it now ends with that callback, as an enterWith there does: the copy schedules the tick that clears the main slot, so the store does not reach later callbacks. --- packages/runtime/src/scr_async.c | 47 +++++++++++--------- tests/corpus/7106-async-prefix-enter-with.ts | 33 +++++++++++++- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 9d3a254696..9e3365fbe6 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -674,6 +674,32 @@ void scr_als_ctx_release(ScrAlsCtx *c) { free(c); } +/* enterWith on the main stack lasts until that code returns to the loop, + * so the store does not reach callbacks scheduled without it: the loop + * clears the main slot when the main module's body returns, and an + * enterWith inside a callback the loop runs puts a tick that clears it at + * the front of the tick queue, so it runs as soon as that callback + * returns. A store that an async callee's prefix or a generator step + * enters becomes the caller's at scr_als_prefix_end and is cleared the + * same way. */ +static SCR_TL bool scr_loop_started = false; + +static void scr_als_main_clear(void) { + scr_als_ctx_release(scr_als_main_slot); + scr_als_main_slot = NULL; +} + +static void scr_next_tick_raw_first(void (*fn)(void)); + +/* A new store on the main slot: one a loop callback enters ends with it. */ +static void scr_als_main_entered(void) { + if (scr_loop_started) scr_next_tick_raw_first(scr_als_main_clear); +} + +void scr_als_entered_with(void) { + if (SCR_ALS_SLOT() == &scr_als_main_slot) scr_als_main_entered(); +} + /* The end of an async body's synchronous prefix: its first await, or its * end. Node runs that prefix inside the caller's execution, so a store it * entered (enterWith) is still the caller's store after the call; the body @@ -685,6 +711,7 @@ static void scr_als_prefix_end(ScrFiber *f) { if (*slot != f->als) { scr_als_ctx_release(*slot); *slot = scr_als_ctx_retain(f->als); + if (f->als != NULL && slot == &scr_als_main_slot) scr_als_main_entered(); } } @@ -758,26 +785,6 @@ ScrDyn *scr_als_peek(double id) { return NULL; } -/* enterWith on the main stack lasts until that code returns to the loop, - * so the store does not reach callbacks scheduled without it: the loop - * clears the main slot when the main module's body returns, and an - * enterWith inside a callback the loop runs puts a tick that clears it at - * the front of the tick queue, so it runs as soon as that callback - * returns. */ -static SCR_TL bool scr_loop_started = false; - -static void scr_als_main_clear(void) { - scr_als_ctx_release(scr_als_main_slot); - scr_als_main_slot = NULL; -} - -static void scr_next_tick_raw_first(void (*fn)(void)); - -void scr_als_entered_with(void) { - if (scr_loop_started && SCR_ALS_SLOT() == &scr_als_main_slot) - scr_next_tick_raw_first(scr_als_main_clear); -} - static SCR_TL ScrCtx scr_loop_ctx; /* the main stack (scheduler home) */ static SCR_TL ScrFiber *scr_current = NULL; /* A finished fiber whose last switch went to the loop (scr_resume_fiber). */ diff --git a/tests/corpus/7106-async-prefix-enter-with.ts b/tests/corpus/7106-async-prefix-enter-with.ts index 27dff250bd..d708221713 100644 --- a/tests/corpus/7106-async-prefix-enter-with.ts +++ b/tests/corpus/7106-async-prefix-enter-with.ts @@ -3,7 +3,9 @@ // after the call (Node's semantics), and the callee keeps it after its own // await; after the first await it stays inside the callee. Covers callees // that never suspend, stackless and fiber-backed callees, nested callees, -// run() scopes inside the prefix, and calls without an await. +// run() scopes inside the prefix, and calls without an await. A store that +// an event-loop callback enters this way, or through a generator step, ends +// with that callback and does not reach the next one. import { AsyncLocalStorage } from "node:async_hooks"; const store = new AsyncLocalStorage(); @@ -45,6 +47,33 @@ async function fibered(tag: string): Promise { return n; } +function* steps(): Generator { + store.enterWith("generator-step"); + yield 1; +} +function callbacks(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + void prefixThenAwait("9"); + log.push(`timer after the call: ${current()}`); + }, 0); + setTimeout(() => { + log.push(`second timer: ${current()}`); + steps().next(); + log.push(`second timer after a generator step: ${current()}`); + }, 0); + setTimeout(() => { + log.push(`third timer: ${current()}`); + void neverSuspends("10"); + log.push(`third timer after the call: ${current()}`); + }, 0); + setTimeout(() => { + log.push(`last timer: ${current()}`); + resolve(); + }, 0); + }); +} + async function main(): Promise { await store.run("A", async () => { await neverSuspends("1"); @@ -83,6 +112,8 @@ async function main(): Promise { await fibered("8"); log.push(`H after fibered: ${current()}`); }); + await callbacks(); + log.push(`main after the callbacks: ${current()}`); for (const line of log) console.log(line); } void main(); From fb544eb98e40024a1ed8122dc8ab41501c0484b9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 06:07:58 -0500 Subject: [PATCH 120/395] Validate DiffieHellman peer keys like Node: oversized keys and RFC 5114 subgroups Keys, primes and generators drop their leading zero bytes before import, as BN_bin2bn does, and a value still past mbedTLS's 80000-byte limit throws Node's error or a coded SC2020 error instead of aborting the process: computeSecret rejects a key longer than the prime as too large, a long generator is a bad generator, and setPublicKey/setPrivateKey refuse with SC2020. Under the RFC 5114 groups, computeSecret rejects a peer key with key^q mod p != 1 with ERR_CRYPTO_INVALID_KEYTYPE, as OpenSSL's public key check does, closing small-subgroup confinement of the private key. The safe-prime groups keep the range check only, as in Node. Exponents past 8192 bits are split into digits bit by bit, so generateKeys with a long private key takes linear time. --- .../limitations/builtin-apis/crypto.mdx | 1 + .../tests/corpus/6712-dh-peer-keys.js.json | 6 + packages/runtime/src/scr_dh.c | 117 +++++++++++++++--- tests/corpus/6712-dh-peer-keys.js | 117 ++++++++++++++++++ .../corpus-costs/6712-dh-peer-keys.js.json | 1 + 5 files changed, 223 insertions(+), 19 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6712-dh-peer-keys.js.json create mode 100644 tests/corpus/6712-dh-peer-keys.js create mode 100644 tests/harness/corpus-costs/6712-dh-peer-keys.js.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 6eb42c8b1e..f1c06eb723 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -20,6 +20,7 @@ ECDH property reads and writes throw a coded SC2020 error. `createDiffieHellman`, `getDiffieHellman`, and `createDiffieHellmanGroup` support the modp1, modp2, modp5, and modp14 through modp18 groups, explicit primes and generators, and generated safe primes, on native targets, through `generateKeys()`, `computeSecret()`, `getPrime()`, `getGenerator()`, `getPublicKey()`, `getPrivateKey()`, `setPublicKey()`, `setPrivateKey()`, and `verifyError`, with Node's private key lengths, DH_check results, and errors; wasm32-wasi builds are refused. Primes longer than 8192 bits throw a coded SC2020 error; Node accepts primes up to 10000 bits for key generation and secrets. + `setPublicKey()` and `setPrivateKey()` keys longer than 80000 bytes, not counting leading zero bytes, throw a coded SC2020 error, as does a generator that long with a prime below 512 bits; Node accepts them. A call on a DiffieHellman value whose arguments no @types/node overload accepts, or whose output encoding is typed `any`, is refused at compile time, as are its `Object.prototype` methods; other property reads, and property writes, throw a coded SC2020 error. `Object.keys()` and `in` do not see a DiffieHellman's own `verifyError` property, and `JSON.stringify()` and `instanceof` are refused. After `createDiffieHellman(primeLength)` throws `Invalid DH parameters`, the next OpenSSL error Node throws can list the failed generation's errors in `opensslErrorStack`; errors in a compiled binary list only their own. diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6712-dh-peer-keys.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6712-dh-peer-keys.js.json new file mode 100644 index 0000000000..101260f548 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6712-dh-peer-keys.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6712-dh-peer-keys.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dh.c b/packages/runtime/src/scr_dh.c index 351d97a0f3..cf44338f56 100644 --- a/packages/runtime/src/scr_dh.c +++ b/packages/runtime/src/scr_dh.c @@ -17,7 +17,8 @@ * p ≡ 23 (mod 24) for the generator 2, p ≡ 59 (mod 60) for 5 and * p ≡ 11 (mod 12) otherwise, as OpenSSL's dh_builtin_genparams does. * Primes longer than mbedTLS's 8192-bit limit throw a coded SC2020 - * error. All arguments are borrowed. */ + * error, as do keys and generators past mbedTLS's 80000-byte limit (leading + * zero bytes aside). All arguments are borrowed. */ #include "scr_runtime.h" #include @@ -39,6 +40,9 @@ #define SD_MIN_MODULUS_BITS 512 #define SD_MAX_MODULUS_BITS 10000 +/* The longest value an mbedtls_mpi holds, in bytes. */ +#define SD_MAX_VALUE_BYTES (MBEDTLS_MPI_MAX_LIMBS * sizeof(mbedtls_mpi_uint)) + /* DH_check result bits, as verifyError reports them. */ enum { SD_P_NOT_PRIME = 0x01, @@ -171,6 +175,14 @@ static void sd_too_long(void) { scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); } +/* A key or generator past SD_MAX_VALUE_BYTES, which BN_bin2bn reads. */ +static void sd_value_too_long(void) { + char message[96]; + int n = snprintf(message, sizeof message, "DiffieHellman values longer than %zu bytes have no native implementation", + (size_t)SD_MAX_VALUE_BYTES); + 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) { bool integer = isfinite(value) && trunc(value) == value; @@ -215,10 +227,10 @@ static bool sd_exp(mbedtls_mpi *out, const mbedtls_mpi *base, const mbedtls_mpi MBEDTLS_MPI_CHK(mbedtls_mpi_lset(&step, 1)); MBEDTLS_MPI_CHK(mbedtls_mpi_shift_l(&step, SD_DIGIT_BITS)); for (size_t offset = 0; offset < ebits; offset += SD_DIGIT_BITS) { - MBEDTLS_MPI_CHK(mbedtls_mpi_copy(&digit, e)); - MBEDTLS_MPI_CHK(mbedtls_mpi_shift_r(&digit, offset)); - for (size_t bit = SD_DIGIT_BITS; bit < mbedtls_mpi_bitlen(&digit); bit++) - MBEDTLS_MPI_CHK(mbedtls_mpi_set_bit(&digit, bit, 0)); + /* Bit by bit, so each digit costs its own width, not the exponent's. */ + MBEDTLS_MPI_CHK(mbedtls_mpi_lset(&digit, 0)); + for (size_t bit = 0; bit < SD_DIGIT_BITS && offset + bit < ebits; bit++) + if (mbedtls_mpi_get_bit(e, offset + bit)) MBEDTLS_MPI_CHK(mbedtls_mpi_set_bit(&digit, bit, 1)); MBEDTLS_MPI_CHK(mbedtls_mpi_exp_mod(&term, &power, &digit, p, NULL)); MBEDTLS_MPI_CHK(mbedtls_mpi_mul_mpi(&acc, &acc, &term)); MBEDTLS_MPI_CHK(mbedtls_mpi_mod_mpi(&acc, &acc, p)); @@ -234,6 +246,36 @@ static bool sd_exp(mbedtls_mpi *out, const mbedtls_mpi *base, const mbedtls_mpi return ret == 0; } +/* Drops a big-endian value's leading zero bytes, which BN_bin2bn ignores + * but mbedtls_mpi_read_binary counts against SD_MAX_VALUE_BYTES. */ +static void sd_strip(const unsigned char **data, size_t *len) { + while (*len > 0 && **data == 0) { + (*data)++; + (*len)--; + } +} + +/* BN_bin2bn: false when the value, without its leading zeros, is longer + * than SD_MAX_VALUE_BYTES. */ +static bool sd_read(mbedtls_mpi *x, const ScrBytes *bytes) { + const unsigned char *data = bytes->data; + size_t len = bytes->len; + sd_strip(&data, &len); + if (len > SD_MAX_VALUE_BYTES) return false; + if (mbedtls_mpi_read_binary(x, data, len) != 0) scr_trap("scriptc: out of memory\n"); + return true; +} + +/* BN_cmp over two big-endian values, for ones sd_read cannot hold. */ +static int sd_cmp_bytes(const ScrBytes *a, const ScrBytes *b) { + const unsigned char *a_data = a->data, *b_data = b->data; + size_t a_len = a->len, b_len = b->len; + sd_strip(&a_data, &a_len); + sd_strip(&b_data, &b_len); + if (a_len != b_len) return a_len < b_len ? -1 : 1; + return a_len ? memcmp(a_data, b_data, a_len) : 0; +} + /* A uniformly random number of exactly `bits` bits (the top bit set), and * odd when `odd`, as BN_priv_rand writes it. */ static bool sd_random_bits(mbedtls_mpi *x, size_t bits, bool odd) { @@ -872,12 +914,21 @@ static ScrDyn *sd_generate(double bits, const ScrBytes *generator_bytes, double /* new DiffieHellman(prime, generator) over explicit parameters. */ static ScrDyn *sd_from_prime(const ScrBytes *prime, const ScrBytes *generator_bytes, double generator) { ScDh *d = sd_alloc(); - if (mbedtls_mpi_read_binary(&d->p, prime->data, prime->len) != 0) scr_trap("scriptc: out of memory\n"); - if (generator_bytes) { - if (mbedtls_mpi_read_binary(&d->g, generator_bytes->data, generator_bytes->len) != 0) - scr_trap("scriptc: out of memory\n"); - } else if (mbedtls_mpi_lset(&d->g, (mbedtls_mpi_sint)generator) != 0) { - scr_trap("scriptc: out of memory\n"); + bool p_held = sd_read(&d->p, prime); + bool g_held = true; + if (generator_bytes) g_held = sd_read(&d->g, generator_bytes); + else if (mbedtls_mpi_lset(&d->g, (mbedtls_mpi_sint)generator) != 0) scr_trap("scriptc: out of memory\n"); + if (!p_held || !g_held) { + /* A prime this long is past MBEDTLS_MPI_MAX_BITS, and a generator + * this long is at least a prime of SD_MIN_MODULUS_BITS. */ + bool bad = g_held ? mbedtls_mpi_cmp_int(&d->g, 2) < 0 + : !p_held ? sd_cmp_bytes(generator_bytes, prime) >= 0 + : mbedtls_mpi_bitlen(&d->p) >= SD_MIN_MODULUS_BITS; + sd_release(d); + if (bad) sd_bad_generator(); + else if (!p_held) sd_too_long(); + else sd_value_too_long(); + return NULL; } if (mbedtls_mpi_cmp_int(&d->g, 2) < 0 || (mbedtls_mpi_bitlen(&d->p) >= SD_MIN_MODULUS_BITS && mbedtls_mpi_cmp_mpi(&d->g, &d->p) >= 0)) { @@ -1057,10 +1108,27 @@ static bool sd_derives(const ScDh *d) { return mbedtls_mpi_cmp_int(&d->priv, 0) != 0 && bits >= SD_MIN_MODULUS_BITS && bits <= SD_MAX_MODULUS_BITS; } +/* DH_check_pub_key's subgroup test: under a named group with its own q + * (the RFC 5114 groups), a peer key must have key^q mod p = 1. The safe + * prime groups' q is (p - 1) / 2, which OpenSSL does not test. */ +static bool sd_in_subgroup(const ScDh *d, const mbedtls_mpi *peer) { + if (!d->named || !d->named->q) return true; + mbedtls_mpi q, r; + mbedtls_mpi_init(&q); + mbedtls_mpi_init(&r); + if (mbedtls_mpi_read_binary(&q, d->named->q, d->named->q_len) != 0 || !sd_exp(&r, peer, &q, &d->p)) + scr_trap("scriptc: out of memory\n"); + bool in = mbedtls_mpi_cmp_int(&r, 1) == 0; + mbedtls_mpi_free(&q); + mbedtls_mpi_free(&r); + return in; +} + /* computeSecret(key, inputEncoding, outputEncoding): key^priv mod p, - * padded to the prime's length. A secret of 0, 1 or p - 1 is predictable - * (a peer key of small order, or a composite modulus), so it fails like - * OpenSSL's derivation does. */ + * padded to the prime's length. A key outside [2, p - 2], or outside a + * named group's order-q subgroup, is rejected as DH_check_pub_key does. + * A secret of 0, 1 or p - 1 is predictable (a peer key of small order, + * or a composite modulus), so it fails like OpenSSL's derivation does. */ static ScrDyn *sd_compute_secret(ScDh *d, const ScrDyn *key, const ScrDyn *in_enc, const ScrDyn *out_enc) { ScrBytes *bytes = ca_key_bytes(SD_SUBJECT, key, in_enc); if (!bytes) return NULL; @@ -1069,12 +1137,19 @@ static ScrDyn *sd_compute_secret(ScDh *d, const ScrDyn *key, const ScrDyn *in_en mbedtls_mpi_init(&peer); mbedtls_mpi_init(&bound); mbedtls_mpi_init(&secret); - if (mbedtls_mpi_read_binary(&peer, bytes->data, bytes->len) != 0 || mbedtls_mpi_sub_int(&bound, &d->p, 1) != 0) + /* A key longer than p, past its leading zeros, is above p - 1. */ + const unsigned char *data = bytes->data; + size_t len = bytes->len; + sd_strip(&data, &len); + bool longer = len > mbedtls_mpi_size(&d->p); + if ((!longer && mbedtls_mpi_read_binary(&peer, data, len) != 0) || mbedtls_mpi_sub_int(&bound, &d->p, 1) != 0) scr_trap("scriptc: out of memory\n"); - if (mbedtls_mpi_cmp_int(&peer, 1) <= 0) { + if (!longer && mbedtls_mpi_cmp_int(&peer, 1) <= 0) { sd_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYLEN", "Supplied key is too small"); - } else if (mbedtls_mpi_cmp_mpi(&peer, &bound) >= 0) { + } else if (longer || mbedtls_mpi_cmp_mpi(&peer, &bound) >= 0) { sd_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYLEN", "Supplied key is too large"); + } else if (!sd_in_subgroup(d, &peer)) { + sd_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYTYPE", "Supplied key is invalid"); } else if (!d->has_priv || (!d->group && !sd_derives(d)) || !sd_exp(&secret, &peer, &d->priv, &d->p) || mbedtls_mpi_cmp_int(&secret, 1) <= 0 || mbedtls_mpi_cmp_mpi(&secret, &bound) == 0) { sd_operation_failed("Failed to compute shared secret"); @@ -1103,9 +1178,13 @@ static ScrDyn *sd_get_value(const mbedtls_mpi *value, bool present, const char * static ScrDyn *sd_set_value(mbedtls_mpi *value, bool *present, ScrDyn *self, const ScrDyn *key, const ScrDyn *encoding) { ScrBytes *bytes = ca_key_bytes(SD_SUBJECT, key, encoding); if (!bytes) return NULL; - if (mbedtls_mpi_read_binary(value, bytes->data, bytes->len) != 0) scr_trap("scriptc: out of memory\n"); - *present = true; + bool held = sd_read(value, bytes); scr_bytes_release(bytes); + if (!held) { + sd_value_too_long(); + return NULL; + } + *present = true; return scr_dyn_retain(self); } diff --git a/tests/corpus/6712-dh-peer-keys.js b/tests/corpus/6712-dh-peer-keys.js new file mode 100644 index 0000000000..819fcf9601 --- /dev/null +++ b/tests/corpus/6712-dh-peer-keys.js @@ -0,0 +1,117 @@ +// DiffieHellman computeSecret's peer key checks, and keys, primes and +// generators with leading zero bytes: under the RFC 5114 groups a peer key +// outside the order-q subgroup is invalid, while other parameters only +// check the range; a key longer than the prime is too large, and leading +// zeros of any length are ignored. +import crypto from "node:crypto"; + +function attempt(label, run) { + try { + console.log(label, "ok", JSON.stringify(run())); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +const zeros = Buffer.alloc(100000); +const padded = (bytes) => Buffer.concat([zeros, bytes]); + +// Each group's parameters, and a peer key of small order: h^((p - 1) / r) +// for a small prime r dividing (p - 1) / q. +const groups = [ + { + label: "RFC 5114 1024-bit", + order: 7, + p: "B10B8F96A080E01DDE92DE5EAE5D54EC52C99FBCFB06A3C69A6A9DCA52D23B616073E28675A23D189838EF1E2EE652C013ECB4AEA906112324975C3CD49B83BFACCBDD7D90C4BD7098488E9C219A73724EFFD6FAE5644738FAA31A4FF55BCCC0A151AF5F0DC8B4BD45BF37DF365C1A65E68CFDA76D4DA708DF1FB2BC2E4A4371", + g: "A4D1CBD5C3FD34126765A442EFB99905F8104DD258AC507FD6406CFF14266D31266FEA1E5C41564B777E690F5504F213160217B4B01B886A5E91547F9E2749F4D7FBD7D3B9A92EE1909D0D2263F80A76A6A24C087A091F531DBF0A0169B6A28AD662A4D18E73AFA32D779D5918D08BC8858F4DCEF97C2A24855E6EEB22B3B2E5", + small: "24986e0c35fbae0d16a21b2aab7eac0dc32e9ca8c24c1eb9387366ee8a6df2bee49b6f46ea2525d208c1bc3d87e3749586616e0161a305f670830ba1fa9e4c5a1bd5ae9f4dc2032f1fd99bbcf3f4444a09d4da3ccbde680580ee8d78feced92d1efb235fc9f25c8525a4e5a453ccf7375119cee48b05421c093d10c8b6e90ab8", + }, + { + label: "RFC 5114 2048-bit, 224-bit q", + order: 3, + p: "AD107E1E9123A9D0D660FAA79559C51FA20D64E5683B9FD1B54B1597B61D0A75E6FA141DF95A56DBAF9A3C407BA1DF15EB3D688A309C180E1DE6B85A1274A0A66D3F8152AD6AC2129037C9EDEFDA4DF8D91E8FEF55B7394B7AD5B7D0B6C12207C9F98D11ED34DBF6C6BA0B2C8BBC27BE6A00E0A0B9C49708B3BF8A317091883681286130BC8985DB1602E714415D9330278273C7DE31EFDC7310F7121FD5A07415987D9ADC0A486DCDF93ACC44328387315D75E198C641A480CD86A1B9E587E8BE60E69CC928B2B9C52172E413042E9B23F10B0E16E79763C9B53DCF4BA80A29E3FB73C16B8E75B97EF363E2FFA31F71CF9DE5384E71B81C0AC4DFFE0C10E64F", + g: "AC4032EF4F2D9AE39DF30B5C8FFDAC506CDEBE7B89998CAF74866A08CFE4FFE3A6824A4E10B9A6F0DD921F01A70C4AFAAB739D7700C29F52C57DB17C620A8652BE5E9001A8D66AD7C17669101999024AF4D027275AC1348BB8A762D0521BC98AE247150422EA1ED409939D54DA7460CDB5F6C6B250717CBEF180EB34118E98D119529A45D6F834566E3025E316A330EFBB77A86F0C1AB15B051AE3D428C8F8ACB70A8137150B8EEB10E183EDD19963DDD9E263E4770589EF6AA21E7F5F2FF381B539CCE3409D13CD566AFBB48D6C019181E1BCFE94B30269EDFE72FE9B6AA4BD7B5A0F1C71CFFF4C19C418E1F6EC017981BC087F2A7065B384B890D3191F2BFA", + small: "73ef184cd50bd427992f4bbeaf1d7712f8cb8264929f533ded004618b29064e3583963fc634df866ef9aa5f158077765219401ad4bf0a2c42204241c8a728af1012cfd399a76f13a0e2cb153c0888a9eb0e05312610923777d185f46bf865bbfe86a0e79a6761d7e153cb46e2221dc3415a7e78c22d33c3d1e2363c041bb57badd344969ee2a1c4917d26ab51a20bc343b4db9964871b0918b2b8e97ff7d297e8b8ca2329426755fdf302a6401030c0f0206a0da004cac53d4408d2ff47cff332435d1f01cbeee301b8dda896be231fd38264f1a5c2b51365a8e74e608a2510c26c92c8d091e18577e854c43d5dc3b2779552f6723878deb5ec58bce2da61abe", + }, + { + label: "RFC 5114 2048-bit, 256-bit q", + order: 7, + p: "87A8E61DB4B6663CFFBBD19C651959998CEEF608660DD0F25D2CEED4435E3B00E00DF8F1D61957D4FAF7DF4561B2AA3016C3D91134096FAA3BF4296D830E9A7C209E0C6497517ABD5A8A9D306BCF67ED91F9E6725B4758C022E0B1EF4275BF7B6C5BFC11D45F9088B941F54EB1E59BB8BC39A0BF12307F5C4FDB70C581B23F76B63ACAE1CAA6B7902D52526735488A0EF13C6D9A51BFA4AB3AD8347796524D8EF6A167B5A41825D967E144E5140564251CCACB83E6B486F6B3CA3F7971506026C0B857F689962856DED4010ABD0BE621C3A3960A54E710C375F26375D7014103A4B54330C198AF126116D2276E11715F693877FAD7EF09CADB094AE91E1A1597", + g: "3FB32C9B73134D0B2E77506660EDBD484CA7B18F21EF205407F4793A1A0BA12510DBC15077BE463FFF4FED4AAC0BB555BE3A6C1B0C6B47B1BC3773BF7E8C6F62901228F8C28CBB18A55AE31341000A650196F931C77A57F2DDF463E5E9EC144B777DE62AAAB8A8628AC376D282D6ED3864E67982428EBC831D14348F6F2F9193B5045AF2767164E1DFC967C1FB3F2E55A4BD1BFFE83B9C80D052B985D182EA0ADB2A3B7313D3FE14C8484B1E052588B9B7D2BBD2DF016199ECD06E1557CD0915B3353BBB64E0EC377FD028370DF92B52C7891428CDC67EB6184B523D1DB246C32F63078490F00EF8D647D148D47954515E2327CFEF98C582664B4C0F6CC41659", + small: "7e22fad9cc23b5949616a26b060dcec3557a81d98dc45f51943f4e29b06dd73e6fbf201c01c4c2620b81698048fcb21655d1276bfb402a41c3af50528f2df02b3440a3b7a1855dfe31a549ddce9563ed18fe1530a3a649f87fa4d427d6d2e1b73cf3848177651080f2ca96628fed411c331d9e28d28da5f0c65f2516f9bb4c72e4c9050f5d654bcc0139e66fbc582ae32d345ad84a249d9cea131c6a9af59ecaefbc190ca265394eb8190ff91a6af58327060ca4900829eaa3a1c26f86737d7510bfd55c430bcc1f2db6a62c3bfe1717e5236945c475bb7b36dc2fa5ab06b089325dfd864a6b044622e62a5638ce23f319cf564826ce1e5c1bc1166896c5f205", + }, +]; + +for (const group of groups) { + const p = Buffer.from(group.p, "hex"); + const g = Buffer.from(group.g, "hex"); + const alice = crypto.createDiffieHellman(p, g); + const bob = crypto.createDiffieHellman(p, g); + alice.generateKeys(); + bob.generateKeys(); + const name = group.label; + attempt(`${name}: verifyError`, () => alice.verifyError); + attempt(`${name}: agreed`, () => alice.computeSecret(bob.getPublicKey()).equals(bob.computeSecret(alice.getPublicKey()))); + attempt(`${name}: order ${group.order}`, () => alice.computeSecret(Buffer.from(group.small, "hex")).length); + attempt(`${name}: 2`, () => alice.computeSecret(Buffer.from([2])).length); + attempt(`${name}: p - 2`, () => { + const key = Buffer.from(p); + key[key.length - 1] -= 2; + return alice.computeSecret(key).length; + }); + attempt(`${name}: p - 1`, () => { + const key = Buffer.from(p); + key[key.length - 1] -= 1; + return alice.computeSecret(key).length; + }); + attempt(`${name}: without keys`, () => crypto.createDiffieHellman(p, g).computeSecret(Buffer.from([2])).length); + // The same prime with another generator is not the named group. The + // private key is a multiple of neither 3 nor 7, so the small-order key + // gives a secret other than 1. + const other = crypto.createDiffieHellman(p, 2); + other.setPrivateKey(Buffer.from("0123456789abcdee", "hex")); + attempt(`${name}, generator 2: order ${group.order}`, () => other.computeSecret(Buffer.from(group.small, "hex")).length); + attempt(`${name}, generator 2: 2`, () => other.computeSecret(Buffer.from([2])).length); +} + +// The safe prime groups check only the range: 11 is a quadratic +// non-residue modulo the 2048-bit MODP prime. +const modp14 = crypto.getDiffieHellman("modp14"); +modp14.generateKeys(); +attempt("modp14: non-residue", () => modp14.computeSecret(Buffer.from([11])).length); +const explicit = crypto.createDiffieHellman(modp14.getPrime(), modp14.getGenerator()); +explicit.generateKeys(); +attempt("modp14 explicit: non-residue", () => explicit.computeSecret(Buffer.from([11])).length); + +// Keys longer than the prime, and leading zeros. +const peer = crypto.createDiffieHellman(modp14.getPrime(), 2); +peer.generateKeys(); +const secret = modp14.computeSecret(peer.getPublicKey()); +attempt("100000 bytes of 0xff", () => modp14.computeSecret(Buffer.alloc(100000, 0xff)).length); +attempt("prime and one byte", () => modp14.computeSecret(Buffer.concat([Buffer.from([1]), modp14.getPrime()])).length); +attempt("hex of 100000 bytes", () => modp14.computeSecret(Buffer.alloc(100000, 0x7f).toString("hex"), "hex").length); +attempt("zero-padded key", () => modp14.computeSecret(padded(peer.getPublicKey())).equals(secret)); +attempt("zero-padded hex key", () => modp14.computeSecret(padded(peer.getPublicKey()).toString("hex"), "hex").equals(secret)); +attempt("zeros", () => modp14.computeSecret(zeros).length); +attempt("zero-padded 1", () => modp14.computeSecret(padded(Buffer.from([1]))).length); +attempt("zero-padded p - 1", () => { + const key = Buffer.from(modp14.getPrime()); + key[key.length - 1] -= 1; + return modp14.computeSecret(padded(key)).length; +}); +attempt("zero-padded public key", () => { + const dh = crypto.createDiffieHellman(modp14.getPrime(), 2); + dh.setPublicKey(padded(peer.getPublicKey())); + return dh.getPublicKey().equals(peer.getPublicKey()); +}); +attempt("zero-padded private key", () => { + const dh = crypto.createDiffieHellman(modp14.getPrime(), 2); + dh.setPrivateKey(padded(peer.getPrivateKey())); + return [dh.getPrivateKey().equals(peer.getPrivateKey()), dh.generateKeys().equals(peer.getPublicKey())]; +}); +attempt("zero-padded prime and generator", () => { + const dh = crypto.createDiffieHellman(padded(modp14.getPrime()), padded(Buffer.from([2]))); + return [dh.verifyError, dh.getPrime().equals(modp14.getPrime()), dh.getGenerator("hex")]; +}); +attempt("generator of 100000 bytes", () => crypto.createDiffieHellman(modp14.getPrime(), Buffer.alloc(100000, 0xff)).verifyError); +attempt("zero-padded generator 1", () => crypto.createDiffieHellman(modp14.getPrime(), padded(Buffer.from([1]))).verifyError); diff --git a/tests/harness/corpus-costs/6712-dh-peer-keys.js.json b/tests/harness/corpus-costs/6712-dh-peer-keys.js.json new file mode 100644 index 0000000000..429593dc5b --- /dev/null +++ b/tests/harness/corpus-costs/6712-dh-peer-keys.js.json @@ -0,0 +1 @@ +{ "plain": 9427, "san": 12410 } From d01117fa517daca56dbb8d87d3c6267d028a81b5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 06:13:45 -0500 Subject: [PATCH 121/395] Batch the contextual types and signatures that lowering asks per node ClassProtocols' identity-flow fixpoint asks the contextual type of each site whose source carries identity, one round trip at a time, and lowering asks the resolved signature of each call and the contextual type of each array literal and conditional it lowers the same way. The checker facade can now batch them: prefetchContextualTypes for the fixpoint's sites before it runs, and, for each prefetched wave of reachable roots, the resolved signatures of its calls and the contextual types of its array literals and conditionals. A batched query that fails is left out of the memo, so a direct query still throws where it is made. The batches use the checker's getContextualTypes and getResolvedSignatures requests. NODE_BATCH_QUERIES keeps them off until scriptc depends on a tsc-ts release that answers them; while it is off the prefetches do nothing. --- .../src/frontend/lowering/class-protocols.ts | 30 +++++++- packages/compiler/src/frontend/ts7/checker.ts | 72 ++++++++++++++++++- .../src/frontend/ts7/semantic-checker.ts | 18 +++++ 3 files changed, 115 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts index 81aa90a1f1..76091b13e5 100644 --- a/packages/compiler/src/frontend/lowering/class-protocols.ts +++ b/packages/compiler/src/frontend/lowering/class-protocols.ts @@ -101,6 +101,8 @@ export class ClassProtocols { const sites: { source: ts.Expression; target: () => ts.Type | undefined; + /** The node whose contextual type `target` answers. */ + contextual?: ts.Expression; assertion?: true; }[] = []; /** Expressions whose values are observed as whole objects. */ @@ -122,7 +124,8 @@ export class ClassProtocols { const callbackParams: { fn: ts.SignatureDeclaration; index: number; name: ts.Node }[] = []; const contextual = (expr: ts.Expression) => () => this.checker.getContextualType(expr); const flow = (expr: ts.Expression | undefined): void => { - if (expr !== undefined) sites.push({ source: expr, target: contextual(expr) }); + if (expr !== undefined) + sites.push({ source: expr, target: contextual(expr), contextual: expr }); }; const write = (target: ts.Expression): void => { while (ts.isParenthesizedExpression(target) || ts.isNonNullExpression(target)) @@ -329,7 +332,11 @@ export class ClassProtocols { facts.loose.push({ node: looseArguments[i]!, call: looseCalls[i]!, symbols }); } this.facts = facts; - const pending: { sourceType: ts.Type; target: () => ts.Type | undefined }[] = []; + const pending: { + sourceType: ts.Type; + target: () => ts.Type | undefined; + contextual?: ts.Expression; + }[] = []; /** Views a value of unknown origin is asserted to and that expose a * method of a class the view accepts (see methodViewSources). */ const methodViews = new Set(); @@ -352,7 +359,11 @@ export class ClassProtocols { } continue; } - pending.push({ sourceType, target: site.target }); + pending.push( + site.contextual === undefined + ? { sourceType, target: site.target } + : { sourceType, target: site.target, contextual: site.contextual }, + ); } for (const { fn, index, name } of callbackParams) { attempt(() => { @@ -377,6 +388,19 @@ export class ClassProtocols { // flows and origins are complete. const before = new Set(marked); const targets = new Map(); + // The fixpoint asks the target of each site whose source carries + // identity: one batch asks those of the sites that carry it now, and a + // site that starts to as marks grow asks its own. + attempt(() => + this.checker.prefetchContextualTypes( + pending.flatMap((entry) => + entry.contextual !== undefined && + attempt(() => this.carriesIdentity(entry.sourceType, marked)) === true + ? [entry.contextual] + : [], + ), + ), + ); let changed = true; while (changed) { changed = false; diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index 4f2d60294e..cd43636afc 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -45,7 +45,7 @@ import type { TypePredicate, TypeReference, } from "./semantic-types.js"; -import type { SemanticChecker as Checker } from "./semantic-checker.js"; +import { NODE_BATCH_QUERIES, type SemanticChecker as Checker } from "./semantic-checker.js"; import { symbolNameOf, type SemanticProject as Project } from "./semantic-model.js"; import { isTypeNode, walkPreorder } from "./ast.js"; import { SignatureKind, SyntaxKind, TypeFlags, type SymbolFlags } from "./enums.js"; @@ -79,6 +79,30 @@ function withPanicFence( } } +/** Answers for `items` in chunks, each answer given to `store`. A chunk + * that throws is split until the throwing items are alone; those get no + * answer, so their direct query throws again where it is made. */ +function batchAnswers( + items: readonly I[], + fetch: (chunk: readonly I[]) => readonly (O | undefined)[], + store: (item: I, answer: O | undefined) => void, +): void { + const answer = (chunk: readonly I[]): void => { + let answers: readonly (O | undefined)[]; + try { + answers = fetch(chunk); + } catch { + if (chunk.length === 1) return; + const mid = chunk.length >> 1; + answer(chunk.slice(0, mid)); + answer(chunk.slice(mid)); + return; + } + chunk.forEach((item, index) => store(item, answers[index])); + }; + for (let i = 0; i < items.length; i += BATCH_CHUNK) answer(items.slice(i, i + BATCH_CHUNK)); +} + function chunked(items: readonly T[], fetch: (chunk: readonly T[]) => readonly R[]): R[] { const out: R[] = []; for (let i = 0; i < items.length; i += BATCH_CHUNK) { @@ -322,7 +346,51 @@ export class CheckerFacade { prefetchRoots(roots: readonly Node[]): void { this.ensureActive(); this.markManaged(roots); - this.prefetchNodes(collectNodes(roots, "reachable")); + const nodes = collectNodes(roots, "reachable"); + this.prefetchNodes(nodes); + if (!NODE_BATCH_QUERIES) return; + // Lowering a call resolves its signature (reconcileOverloadReturn). + this.prefetchResolvedSignatures( + nodes.filter( + (node) => + node.kind === SyntaxKind.CallExpression || + node.kind === SyntaxKind.TaggedTemplateExpression, + ), + ); + // Lowering an array literal or a conditional asks its contextual type. + this.prefetchContextualTypes( + nodes.filter( + (node) => + node.kind === SyntaxKind.ArrayLiteralExpression || + node.kind === SyntaxKind.ConditionalExpression, + ), + ); + } + + /** Batches the contextual type of each node, for an analysis that asks it + * of many nodes (class-protocols.ts). A query that fails stays out of the + * memo, so a later getContextualType asks again as it would have. */ + prefetchContextualTypes(nodes: readonly Node[]): void { + if (!NODE_BATCH_QUERIES) return; + this.ensureActive(); + const missing = [...new Set(nodes)].filter((node) => !this.cache.contextualType.has(node)); + batchAnswers( + missing, + (chunk) => this.raw.getContextualTypes(chunk), + (node, type) => this.cache.contextualType.set(node, type), + ); + } + + /** Batches the resolved signature of each call (getResolvedSignature). */ + prefetchResolvedSignatures(nodes: readonly Node[]): void { + if (!NODE_BATCH_QUERIES) return; + this.ensureActive(); + const missing = [...new Set(nodes)].filter((node) => !this.cache.resolvedSignature.has(node)); + batchAnswers( + missing, + (chunk) => this.raw.getResolvedSignatures(chunk), + (node, signature) => this.cache.resolvedSignature.set(node, signature), + ); } /** Batches symbol queries for every identifier under roots without the diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.ts b/packages/compiler/src/frontend/ts7/semantic-checker.ts index 5e7f8339a8..1e5f46b4d1 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.ts @@ -96,6 +96,13 @@ export interface SemanticCompletion { symbol: SemanticSymbol | undefined; } +/** Whether lowering asks the checker for contextual types and resolved + * signatures in batches (`getContextualTypes`, `getResolvedSignatures`). + * Off until scriptc depends on a tsc-ts release that answers them: the + * batch prefetches (checker.ts) then do nothing and lowering asks node by + * node. */ +export const NODE_BATCH_QUERIES = false; + /** The pinned checker's request surface over native objects. Every query * converges on the same snapshot/project registries, including batch and * lazy-object queries. No SDK object is constructed or consulted here. */ @@ -201,6 +208,10 @@ export class SemanticChecker { getContextualType(node: AstNode): SemanticType | undefined { return this.type("getContextualType", { location: node.id }); } + /** getContextualType for each node. */ + getContextualTypes(nodes: readonly AstNode[]): (SemanticType | undefined)[] { + return this.optionalTypes("getContextualTypes", { locations: nodes.map((node) => node.id) }); + } getTypeFromTypeNode(node: AstNode): SemanticType | undefined { return this.type("getTypeFromTypeNode", { location: node.id }); } @@ -213,6 +224,13 @@ export class SemanticChecker { getResolvedSignature(node: AstNode): SemanticSignature | undefined { return this.signature("getResolvedSignature", { location: node.id }); } + /** getResolvedSignature for each node. */ + getResolvedSignatures(nodes: readonly AstNode[]): (SemanticSignature | undefined)[] { + const data = this.project.request<(SignatureResponse | null)[]>("getResolvedSignatures", { + locations: nodes.map((node) => node.id), + }); + return data.map((item) => (item === null ? undefined : this.project.signature(item))); + } getSignatureFromDeclaration(node: AstNode): SemanticSignature | undefined { return this.signature("getSignatureFromDeclaration", { location: node.id }); } From 0f6ea63f21f8a0f044177ebb6f36fc3cf96fac16 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 06:17:05 -0500 Subject: [PATCH 122/395] Give each evaluation of a forward-read generic binding its own function A generic binding inside a function that a nested function read before the declaration lowered stores its instance into a TDZ box when the declaration runs. That value was the instance's closure, which the backend interns for module functions, so every evaluation of the declaration stored the same function object. JavaScript makes a new one each time. The stored value is now a closure over a lifted forwarder that calls the instance, so each evaluation allocates a new closure; the forwarder is created once per instance. Async and rest-parameter bindings read this way report SC1090. Corpus 7180 checks the identities. --- .../frontend/lowering/generic-functions.ts | 71 ++++++++++++++++++- .../7180-generic-local-binding-early-uses.ts | 16 +++++ 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index d0e07582c9..e8d3241c03 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -16,7 +16,7 @@ import { isUnitType, typeEquals, } from "../../ir/ir.js"; -import { nodeThrowExpr } from "../../ir/build.js"; +import { helperLoc, nodeThrowExpr } from "../../ir/build.js"; import { isJsSourceFile, locOf } from "../program.js"; import { typeKey } from "../type-mapper.js"; import { functionCanReturnUndefined } from "../function-completion.js"; @@ -102,6 +102,8 @@ export interface GenericFnInfo { * runs: the BOOL global its declaration sets, and whether it is a * `let`/`const` (a temporal dead zone) or a `var` (undefined). */ presence?: { flagId: string; lexical: boolean }; + /** genericValueForwarder's lifted functions, by instance name. */ + valueForwarders?: Map; /** runsAfterDeclaration's answers for the function declarations whose * calls it placed. */ ordering?: Map; @@ -1266,7 +1268,10 @@ export function lowerGenericFnValue( * nested function read it before the declaration lowered (`const read = * () => id; const id: (x: number) => number = (x: T): T => x`): such * a read resolves to an ordinary TDZ local, so the declaration assigns it - * the instance its concrete declared signature pins. */ + * the instance its concrete declared signature pins. Each evaluation of + * the declaration makes a new function object, as in JavaScript: the + * value is a closure over a forwarder to the instance, not the instance's + * interned closure. */ export function genericBindingStoredValue( lowerer: Lowerer, decl: ts.VariableDeclaration, @@ -1281,7 +1286,67 @@ export function genericBindingStoredValue( ); const sigs = lowerer.checker.getCallSignatures(lowerer.typeOf(name)); if (sigs.length !== 1) fenceUnpinned(); - return pinnedGenericClosure(lowerer, name, info, sigs[0]!, fenceUnpinned); + const pinned = pinnedGenericClosure(lowerer, name, info, sigs[0]!, fenceUnpinned); + const async = ts.getModifiers(info.decl)?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); + if (pinned.kind !== "closure" || pinned.type.kind !== "func" || pinned.type.rest || async) + return lowerer.unsupported( + "SC1090", + name, + `async or rest-parameter generic function bindings read before their declaration (each evaluation of '${info.baseName}' makes a new function object)`, + ); + return { + ...pinned, + fnName: genericValueForwarder(lowerer, info, pinned.fnName, pinned.type, pinned.loc), + }; +} + +/** A lifted function that calls the instance `instance` (of type `type`) + * with its own arguments. Lifted functions get a new closure object per + * evaluation; module functions, which instances are, get one interned + * closure. */ +function genericValueForwarder( + lowerer: Lowerer, + info: GenericFnInfo, + instance: string, + type: IrType & { kind: "func" }, + at: SrcLoc, +): string { + info.valueForwarders ??= new Map(); + const existing = info.valueForwarders.get(instance); + if (existing !== undefined) return existing; + const name = `${instance}%value`; + const loc = helperLoc(at); + const params: IrParam[] = type.params.map((paramType, i) => ({ + localId: `%arg${i}.0`, + name: `%arg${i}`, + type: paramType, + })); + const call: IrExpr = { + kind: "call", + callee: instance, + args: params.map((p): IrExpr => ({ kind: "varRef", localId: p.localId, type: p.type, loc })), + type: type.ret, + loc, + }; + lowerer.liftedFns.push({ + name, + params, + returnType: type.ret, + locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), + captures: [], + ...(ts.isFunctionExpression(info.decl) ? { ownsPrototype: true } : {}), + body: + type.ret.kind === "void" + ? [ + { kind: "exprStmt", expr: call, loc }, + { kind: "return", value: null, loc }, + ] + : [{ kind: "return", value: call, loc }], + loc, + }); + lowerer.noteEdge(name); + info.valueForwarders.set(instance, name); + return name; } /** The closure of the instance of `info` that `target` pins, for a value diff --git a/tests/corpus/7180-generic-local-binding-early-uses.ts b/tests/corpus/7180-generic-local-binding-early-uses.ts index 85458d973e..f3f7816b5d 100644 --- a/tests/corpus/7180-generic-local-binding-early-uses.ts +++ b/tests/corpus/7180-generic-local-binding-early-uses.ts @@ -51,3 +51,19 @@ function counter(): () => number { return step; } console.log(counter()()); + +// Each evaluation of the declaration makes a new function object. +const first = make(); +console.log(first === make(), first === first, first(8)); +function looped(): string { + const seen: ((x: string) => string)[] = []; + for (let i = 0; i < 2; i++) { + const read = (): ((x: string) => string) => tag; + const tag: (x: string) => string = function (x: T): T { + return x; + }; + seen.push(read()); + } + return `${seen[0] === seen[1]} ${seen[1]!("p")}`; +} +console.log(looped()); From 3fecf687f542143adbb381bf6f182383b7baa7e8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 06:31:30 -0500 Subject: [PATCH 123/395] Throw Node's TypeError for member reads of holes in wide unions A binding that may hold a value its declared type omits (a function parameter fed array elements, which can be holes) and that the checker narrowed to one record or class arm of a union with more than two arms was read through a checked extraction of that arm. When the binding held a hole, a member read or method call threw "undefined is not representable in the target union ..." for a record arm and "Value is not an instance of 'Shape'" for a class arm, where Node throws "Cannot read properties of undefined (reading 'a')". The read now tests for undefined first and throws Node's TypeError, as the two-arm path already does. --- .../lowering/expressions/narrowing.ts | 49 +++++++++++++++--- .../7280-named-reads-hole-receivers.ts.json | 6 +++ .../corpus/7280-named-reads-hole-receivers.ts | 50 +++++++++++++++++++ .../7280-named-reads-hole-receivers.ts.json | 1 + 4 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7280-named-reads-hole-receivers.ts.json create mode 100644 tests/corpus/7280-named-reads-hole-receivers.ts create mode 100644 tests/harness/corpus-costs/7280-named-reads-hole-receivers.ts.json diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index 2e3dd61408..786f3a3371 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -985,11 +985,41 @@ export function runtimeOptionalReceiverRead( loc, }; } + // An array hole in a slot with more than two arms fails the member + // access with Node's TypeError before the checked extraction runs. + const holeGuarded = (extracted: IrExpr): IrExpr => { + if (local.type.kind !== "union") return extracted; + const holeTag = lowerer.armTag(local.type.unionId, UNDEFINED_T); + if (holeTag < 0) return extracted; + return { + kind: "ternary", + cond: { + kind: "unionIsTag", + unionId: local.type.unionId, + tag: holeTag, + negated: false, + value: varRef(local.id, local.type, loc), + type: BOOL, + loc, + }, + then: nodeThrowExpr( + 1, + "", + runtimeOptionalReceiverMessage(expr, property), + extracted.type, + loc, + ), + else_: extracted, + type: extracted.type, + loc, + }; + }; if (narrowed.kind === "object") { const view = checkedClassAssertion(lowerer, varRef(local.id, local.type, loc), narrowed, loc); // Preserve the member-specific missing-value error on the two-arm // path below; mixed storage also needs to validate its concrete class. - if (view && (lowerer.unions.get(local.type.unionId)?.arms.length ?? 0) > 2) return view; + if (view && (lowerer.unions.get(local.type.unionId)?.arms.length ?? 0) > 2) + return holeGuarded(view); } if (narrowed.kind === "record" && lowerer.armTag(local.type.unionId, narrowed) < 0) { const hosted = hostedStorageView(lowerer, varRef(local.id, local.type, loc), narrowed, loc); @@ -1014,7 +1044,9 @@ export function runtimeOptionalReceiverRead( "a direct read from a runtime-optional capture with multiple value arms", ); } - return lowerer.checkedNarrowCall(helper, varRef(local.id, local.type, loc), narrowed, loc); + return holeGuarded( + lowerer.checkedNarrowCall(helper, varRef(local.id, local.type, loc), narrowed, loc), + ); } // A predicate can strengthen a record's fields or narrow a base class // to a subclass without changing the slot's stored value arm. Extract @@ -1029,10 +1061,7 @@ export function runtimeOptionalReceiverRead( "a runtime-optional receiver without a representable value arm", ); } - const message = - property === null - ? `${expr.text} is not a function` - : `Cannot read properties of undefined (reading '${property}')`; + const message = runtimeOptionalReceiverMessage(expr, property); return lowerer.maybeNarrow( { kind: "ternary", @@ -1061,6 +1090,14 @@ export function runtimeOptionalReceiverRead( ); } +/** Node's TypeError message for a member read (`property`) or a call + * (`property` null) through an undefined runtime-optional receiver. */ +function runtimeOptionalReceiverMessage(expr: ts.Identifier, property: string | null): string { + return property === null + ? `${expr.text} is not a function` + : `Cannot read properties of undefined (reading '${property}')`; +} + export function runtimeOptionalTrueIds(lowerer: Lowerer, node: ts.Expression): IrLocal[] { return runtimeOptionalGuardIds(lowerer, node, true); } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7280-named-reads-hole-receivers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7280-named-reads-hole-receivers.ts.json new file mode 100644 index 0000000000..febb008f78 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7280-named-reads-hole-receivers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7280-named-reads-hole-receivers.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7280-named-reads-hole-receivers.ts b/tests/corpus/7280-named-reads-hole-receivers.ts new file mode 100644 index 0000000000..59f0c4820b --- /dev/null +++ b/tests/corpus/7280-named-reads-hole-receivers.ts @@ -0,0 +1,50 @@ +// Named reads and method calls through a parameter narrowed to one arm of a +// wide union, when an array hole reaches the narrowed branch. +type Bag = number | string | number[] | { a?: number; f?: () => string }; +function isBag(value: Bag): value is { a?: number; f?: () => string } { + return !Array.isArray(value) && typeof value !== "string" && typeof value !== "number"; +} +function peek(value: Bag): string { + if (isBag(value)) return typeof value.a; + return "-"; +} +function call(value: Bag): string { + if (isBag(value)) return value.f?.() ?? "none"; + return "-"; +} +class Shape { + side: number; + constructor(side: number) { + this.side = side; + } + area(): number { + return this.side * this.side; + } +} +type Item = Shape | string | number; +function area(item: Item): number { + if (item instanceof Shape || (typeof item !== "string" && typeof item !== "number")) return item.area(); + return 0; +} +const bags: Bag[] = [{ a: 1, f: () => "f" }, "s", [1]]; +bags.length = 4; +const items: Item[] = [new Shape(2), "x"]; +items.length = 3; +const out: string[] = []; +for (let i = 0; i < bags.length; i++) { + for (const which of [0, 1]) { + try { + out.push(which === 0 ? peek(bags[i]) : call(bags[i])); + } catch (e) { + out.push(e instanceof TypeError ? e.message : "other"); + } + } +} +for (let i = 0; i < items.length; i++) { + try { + out.push(String(area(items[i]))); + } catch (e) { + out.push(e instanceof TypeError ? e.message : "other"); + } +} +console.log(out.join("\n")); diff --git a/tests/harness/corpus-costs/7280-named-reads-hole-receivers.ts.json b/tests/harness/corpus-costs/7280-named-reads-hole-receivers.ts.json new file mode 100644 index 0000000000..db9f235514 --- /dev/null +++ b/tests/harness/corpus-costs/7280-named-reads-hole-receivers.ts.json @@ -0,0 +1 @@ +{ "plain": 4700, "san": 2400 } From 4cb46d6472af2f82aa2161c0ae6bccf438389449 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 06:39:06 -0500 Subject: [PATCH 124/395] 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 83c3ede49a..0cf8bcae5f 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 66373655fb..8ab286873e 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 125/395] 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 3e0b5789d3..170b117937 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 173bdc0c66..de24038cc3 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 310c8f3893..707c916ec8 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 1f74cea429..63175d50a9 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 678b375fee..1555299208 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 d8ccc15b7b..c74969d10d 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 213abfec6a..92c1946d5e 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 c95543c617..7334455aff 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 e814bc0f55..923c857feb 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 0000000000..bd7194022f --- /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 0000000000..9bc7d83428 --- /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 0000000000..a817ad3d2a --- /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 0000000000..aaefe846a3 --- /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 0000000000..41d005bd09 --- /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 0000000000..afed7a70d9 --- /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 0000000000..229c01bcdc --- /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 0000000000..1878554c1e --- /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 0000000000..4da840cbcc --- /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 facf9f2fff..8a29e7c5da 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 80e01b08c5..e978ac7aa6 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 f610ffdbb8..6986330f53 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 99caa31a8f..9e53b1e98d 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 c61df9a90f..cce327508b 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 0000000000..7fb5066edf --- /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 0000000000..c026a6691b --- /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 0000000000..dd13aa3251 --- /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 0000000000..bc19cd0e9c --- /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 0000000000..f11746eda3 --- /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 0000000000..cfc8de8065 --- /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 0000000000..0228955918 --- /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 0000000000..0476e56c72 --- /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 0000000000..8439c563d9 --- /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 2c43e99004..ac1afb2c22 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 6706a1278c..697b1b8ed3 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 0000000000..3c9170116a --- /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 0000000000..a3f53b09c5 --- /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 0000000000..0c65d046c2 --- /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 0000000000..0d9aa6b130 --- /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 0000000000..2769f5b726 --- /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 0000000000..04f903d8ab --- /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 0000000000..78aea9163c --- /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 0000000000..0dd7e966b2 --- /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 0000000000..f2ba6c139f --- /dev/null +++ b/tests/harness/corpus-costs/6922-local-subclass-override-dispatch.mjs.json @@ -0,0 +1 @@ +{ "plain": 3216, "san": 5042 } From 9b317ba97da174cc7949a8c849dd6c0fb5e56879 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 06:48:13 -0500 Subject: [PATCH 126/395] Allow the JSX lowering tests' TypeScript 5 import jsx-source.test.ts compares the JSX rewrite with TypeScript 5's transpileModule output, so it imports typescript5 as the other test oracles on the TypeScript 5 island allowlist do. scripts/test-ts7.mjs did not list it, and the TypeScript 7 parity job failed since #1786. --- scripts/test-ts7.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/test-ts7.mjs b/scripts/test-ts7.mjs index e022c1ff5f..b776bea3ef 100644 --- a/scripts/test-ts7.mjs +++ b/scripts/test-ts7.mjs @@ -25,6 +25,7 @@ if (process.argv.includes("--baselines-only")) { const ALLOWED_TYPESCRIPT5_IMPORTS = new Set([ "packages/compiler/src/frontend/comptime-node.ts", + "packages/compiler/src/frontend/jsx-source.test.ts", "packages/compiler/src/frontend/ts7/world-check.ts", "packages/compiler/src/frontend/ts7/source-parser.test.ts", "packages/compiler/src/library/semantic-source.ts", From 1f357b3091288cf7aafa51d988580fa0b66c6420 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 07:01:51 -0500 Subject: [PATCH 127/395] Fence non-Buffer chunk parameters on byte-mode stream callbacks again --- .../src/frontend/lowering/lower-stream.ts | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 14062051fa..cb81348eb5 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -289,6 +289,7 @@ function lowerStreamCallback( node: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction, thisType: IrType, fullTuple: IrType[], + chunkValues = true, ): IrExpr { const loc = locOf(node); const { shapes, funcType } = lowerer.lambdaSignature(node); @@ -309,11 +310,13 @@ function lowerStreamCallback( // callable dyn whose glue dispatches to the runtime's *_done). if (funcType.params[i]!.kind === "dyn") continue; // A write or transform chunk of another type converts from the - // chunk's value when the callback runs (an object-mode side's value, - // or the Buffer), or throws a TypeError there; an encoding of another - // type (`string | null | undefined`) from the encoding argument's. + // chunk's value when the callback runs (an object-mode side's value), + // or throws a TypeError there — on a side known to be byte mode the + // chunk is a Buffer, so the declared type is the fence's; an encoding + // of another type (`string | null | undefined`) converts from the + // encoding argument's value. if ( - (i === 0 || i === 1) && + ((i === 0 && chunkValues) || i === 1) && (which === "write" || which === "transform") && funcType.params[i]!.kind !== "func" && canConvertToDyn( @@ -465,6 +468,20 @@ 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. */ +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 !== "objectMode" && !(duplexish && name === "writableObjectMode")) continue; + if (prop.initializer.kind !== ts.SyntaxKind.FalseKeyword) return true; + } + return false; +} + /** The full Node parameter tuples of the option callbacks, given the * concrete stream's `this` type. The trailing completion callbacks keep * whatever func type the checker contextually gave the user's parameter @@ -802,7 +819,10 @@ function parseStreamOptions( } return declared ?? funcOf([], VOID); }); - present.set(name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple)); + present.set( + name, + lowerStreamCallback(lowerer, name, fnNode, thisType, tuple, out.writableObject), + ); } finally { if (pinned) lowerer.stageSourceTypes.delete(pinned); } @@ -1151,6 +1171,7 @@ function streamMethodWrapper( thisType: IrType, loc: SrcLoc, blame: ts.Node, + chunkValues = true, ): IrExpr | null { const found = lowerer.findMethodOn(info, methodName); if (!found) { @@ -1235,10 +1256,11 @@ function streamMethodWrapper( continue; } // A write or transform chunk of another type converts from the - // chunk's value when the method runs (an object-mode side's value, - // or the Buffer), or throws a TypeError there. + // chunk's value when the method runs (an object-mode side's value), + // or throws a TypeError there; a byte-mode side's chunk is a Buffer. if ( i === 0 && + chunkValues && (which === "write" || which === "transform") && got.type.kind !== "func" && canConvertToDyn( @@ -1343,10 +1365,20 @@ export function lowerStreamSuperCall( ); } const shape = streamCtorShape(cls); + // Whether the writable side holds values: options read at run time may + // set objectMode, and a literal says so with objectMode (or a Duplex's + // writableObjectMode) true; otherwise _write and _transform receive + // Buffers. + const optsNode = argNodes[0] !== undefined ? followOptionsArg(lowerer, argNodes[0]) : null; + const chunkValues = + argNodes[0] !== undefined && + (optsNode === null || + !ts.isObjectLiteralExpression(optsNode) || + literalWritesValues(optsNode, cls !== "%Readable" && cls !== "%Writable")); const fallback = (name: string): IrExpr | null => { const methodName = UNDERSCORE_METHODS.get(name); return methodName - ? streamMethodWrapper(lowerer, info, name, methodName, thisType, loc, blame) + ? streamMethodWrapper(lowerer, info, name, methodName, thisType, loc, blame, chunkValues) : null; }; const thisRef = (): IrExpr => ({ kind: "varRef", localId: thisLocal.id, type: thisType, loc }); From 01dd4f6796c2deed78f330822c861e4cb73321e3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 07:05:30 -0500 Subject: [PATCH 128/395] Keep the handle tags in name order and update two tests for current main --- packages/runtime/src/scr_dyn.h | 10 +++++----- tests/harness/library-profile.test.ts | 9 ++++++--- tests/harness/stream-entry-kinds.test.ts | 6 ++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index c9d090370e..9607274e32 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -143,6 +143,7 @@ typedef enum { X(ABORT_CONTROLLER, 15) /* native static-fetch AbortController */ \ X(ABORT_SIGNAL, 8) /* native static-fetch AbortSignal */ \ X(ARRAY_BUFFER, 18) /* fixed-length ArrayBuffer storage */ \ + X(ASYMMETRIC_CRYPTO_KEY, 69) /* WebCrypto ECDSA and RSA CryptoKey (scr_webcrypto_pki.c) */ \ X(BLOB, 41) /* ScrBlob — new Blob() and Blob#slice() */ \ X(BROADCAST_CHANNEL, 51) /* worker_threads BroadcastChannel (scr_worker_events.c) */ \ X(BUILTIN_MODULE, 21) /* native builtin export object */ \ @@ -160,11 +161,6 @@ typedef enum { X(DIR, 44) /* fs.Dir (scr_lib.c) */ \ X(DISPOSABLE_STACK, 49) /* DisposableStack (scr_symbol.c) */ \ X(ECDH, 64) /* node:crypto ECDH (scr_ecdh.c) */ \ - X(PRIVATE_KEY, 65) /* node:crypto PrivateKeyObject (scr_pki.c) */ \ - X(PUBLIC_KEY, 66) /* node:crypto PublicKeyObject (scr_pki.c) */ \ - X(SIGN, 67) /* node:crypto Sign (scr_pki.c) */ \ - X(VERIFY, 68) /* node:crypto Verify (scr_pki.c) */ \ - X(ASYMMETRIC_CRYPTO_KEY, 69) /* WebCrypto ECDSA and RSA CryptoKey (scr_webcrypto_pki.c) */ \ X(EVENT, 14) /* ScrEvent — new Event() and AbortSignal's abort events */ \ X(EVENT_TARGET, 39) /* ScrEventTarget — new EventTarget() */ \ X(FETCH_HEADERS, 13) /* native static-fetch response Headers */ \ @@ -192,14 +188,18 @@ typedef enum { X(NET_SERVER, 3) /* ScrNetServer — net.Server (http.Server rides the same handle) */ \ X(NET_SOCKET, 2) /* ScrNetSocket — net.Socket */ \ X(NET_SOCKET_ADDRESS, 47) /* net.SocketAddress (scr_net.c) */ \ + X(PRIVATE_KEY, 65) /* node:crypto PrivateKeyObject (scr_pki.c) */ \ + X(PUBLIC_KEY, 66) /* node:crypto PublicKeyObject (scr_pki.c) */ \ X(REGEXP, 26) /* native RegExp, shared compiled pattern */ \ X(SEARCH_PARAMS, 33) /* live WHATWG URLSearchParams */ \ X(SEGMENTER, 19) /* default Unicode grapheme segmentation */ \ X(SEGMENTS, 20) /* iterable immutable segmentation result */ \ X(SET, 25) /* native Set, shared backing map */ \ X(SHARED_ARRAY_BUFFER, 37) \ + X(SIGN, 67) /* node:crypto Sign (scr_pki.c) */ \ X(STDIO, 27) /* stable process stdin/stdout/stderr values */ \ X(URL, 28) /* native WHATWG URL */ \ + X(VERIFY, 68) /* node:crypto Verify (scr_pki.c) */ \ X(WASM_GLOBAL, 61) /* WebAssembly.Global */ \ X(WASM_INSTANCE, 58) /* WebAssembly.Instance */ \ X(WASM_MEMORY, 59) /* WebAssembly.Memory */ \ diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts index 2b28a6cd06..f327f83724 100644 --- a/tests/harness/library-profile.test.ts +++ b/tests/harness/library-profile.test.ts @@ -482,7 +482,10 @@ describe("library profile fences", () => { remediations: { SC2012: "the explicit map key wins" }, fences: [ { id: "diagnostic.sc2012", remediation: "request it as an effect" }, - { id: "node-builtin.crypto.createSign", remediation: "signatures come from the host" }, + { + id: "node-builtin.crypto.createDiffieHellman", + remediation: "key agreement comes from the host", + }, ], }, }), @@ -492,9 +495,9 @@ describe("library profile fences", () => { // Explicit remediations keys win over fence-supplied text. expect(profileRemediation(r.profile, "SC2012")).toBe("the explicit map key wins"); // A fence's remediation answers for its covered entries' codes - // (crypto.createSign is SC2020) — spec §3's "in a fence entry or a + // (crypto.createDiffieHellman is SC2020) — spec §3's "in a fence entry or a // remediations map", one lookup either way. - expect(profileRemediation(r.profile, "SC2020")).toBe("signatures come from the host"); + expect(profileRemediation(r.profile, "SC2020")).toBe("key agreement comes from the host"); expect(profileRemediation(r.profile, "SC4014")).toBeUndefined(); }); diff --git a/tests/harness/stream-entry-kinds.test.ts b/tests/harness/stream-entry-kinds.test.ts index f7333b298b..ec89c5914f 100644 --- a/tests/harness/stream-entry-kinds.test.ts +++ b/tests/harness/stream-entry-kinds.test.ts @@ -138,7 +138,7 @@ r.once('readable', () => { expect(r.stdout).toBe(`caught ${READ_FENCE_MESSAGE}\n`); }); - test("a dynamic source that mixes Buffers and strings without a decoder is destroyed", async () => { + test("a dynamic source that mixes Buffers and strings yields each as it is, as Node does", async () => { const r = await compileAndRun( "mixed-dynamic-source", `import { Readable } from "node:stream"; @@ -151,8 +151,6 @@ r.on("close", () => console.log("close")); `, ); expect(r.exitCode).toBe(0); - expect(r.stdout).toBe( - "data true b1\nerror a Readable.from source that mixes strings and Buffers is not supported yet\nclose\n", - ); + expect(r.stdout).toBe("data true b1\ndata false s1\nclose\n"); }); }); From e07f1be5c297a17769d7a2b69dcea17b6f78c2f5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 07:07:34 -0500 Subject: [PATCH 129/395] 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 213abfec6a..29086d326b 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 0000000000..cb3d36498f --- /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 0000000000..4810c440e0 --- /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 0000000000..f34efd332f --- /dev/null +++ b/tests/harness/corpus-costs/7006-evolving-array-aliases.ts.json @@ -0,0 +1 @@ +{ "plain": 2292, "san": 3445 } From f87a04fa06b0e2e9b547b435542220a59d2761ae Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 07:08:26 -0500 Subject: [PATCH 130/395] Build typed 'message' listener parameters from the message A 'message' listener with a typed parameter received each message as a checked-dynamic value: the port decoded the message into dyn objects, the listener's thunk matched the value against each union arm, read every field again to build the record, and the dyn objects were then released. For bench/channel-messages that was about a third of the delivery time. - A closure registered with on, once, addListener, prependListener or prependOnceListener for 'message' gets a typed message entry when its parameter is a number, boolean, string, a record of those without union fields, or a boxed union of those arms with null or undefined. The entry builds the parameter from the message's nodes and calls the closure. - The entry handles a message only when it has the parameter's exact layout: the union arm the checked conversion picks, and a record's declared keys in declaration order with no others. Any other message runs through the thunk on the decoded value, as before, so undeclared keys, other orders and mismatches behave as they did. - Ports, workers and parentPort deliver a message through the entries when no value in it is reached twice and it holds no ports or native handles. Listeners without an entry share one decoded value, made when the first of them runs. - The message readers are runtime entry points, so the runtime ABI is now 27. Corpus 7130 and 7131 check typed listeners on MessageChannel ports, a worker and parentPort against Node: exact layouts, extra keys, shared values, nested records, untyped listeners beside typed ones, once listeners and a transferred port. --- packages/cli/test/native-link-info.test.ts | 4 +- packages/compiler/src/backend/llvm/dyn.ts | 300 ++++++++++++++++++ .../compiler/src/backend/llvm/expr-dynamic.ts | 35 ++ packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../test/native-codegen-integration.test.ts | 4 +- .../7130-typed-message-listeners.ts.json | 6 + .../corpus/7131-typed-worker-messages.ts.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_dyn.h | 57 +++- packages/runtime/src/scr_events_emitter.c | 54 +++- packages/runtime/src/scr_events_emitter.h | 6 + packages/runtime/src/scr_json.c | 7 + packages/runtime/src/scr_message.c | 69 ++-- packages/runtime/src/scr_message.test.c | 47 +++ packages/runtime/src/scr_worker_events.c | 24 ++ tests/corpus/7130-typed-message-listeners.ts | 80 +++++ tests/corpus/7131-typed-worker-messages.ts | 54 ++++ .../7130-typed-message-listeners.ts.json | 1 + .../7131-typed-worker-messages.ts.json | 1 + .../self-hosting-native-toolchain.test.ts | 2 +- 25 files changed, 718 insertions(+), 55 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7130-typed-message-listeners.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7131-typed-worker-messages.ts.json create mode 100644 tests/corpus/7130-typed-message-listeners.ts create mode 100644 tests/corpus/7131-typed-worker-messages.ts create mode 100644 tests/harness/corpus-costs/7130-typed-message-listeners.ts.json create mode 100644 tests/harness/corpus-costs/7131-typed-worker-messages.ts.json diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 630ef71733..a7a5e718a0 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/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 6911ef98b8..ac01a9209e 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -108,6 +108,15 @@ const DYN_HANDLE_UNBOX_FN: Record = { dgramSocket: "scr_dgram_from_dyn", }; +/** ScrMessageKind (scr_message.h): the node kinds message readers test. */ +const MESSAGE_KIND = { + UNDEFINED: 0, + NULL: 1, + BOOL: 2, + NUMBER: 3, + STRING: 4, +} as const; + export const DYN_KIND = { NULL: 0, BOOL: 1, @@ -173,6 +182,8 @@ export class LlDyn { private readonly dynFuncThunks = new Map(); private readonly dynFuncBoxes = new Map(); private readonly dynFuncAdapters = new Map(); + private readonly messageListeners = new Map(); + private readonly messageReaders = new Map(); private readonly promiseDynAdapters = new Map(); private readonly mapDynOps = new Map(); private dynToStrFn: string | null = null; @@ -4610,6 +4621,295 @@ export class LlDyn { return name; } + /** `sc_dml_(ptr c, ptr message) -> i1`, the typed message entry of a + * closure registered as a 'message' listener (ScrDynMessageListener): + * builds the closure's parameter from the message's nodes and calls the + * closure. It answers false, without calling it, when the message does + * not have the parameter's exact layout, and the listener then runs + * through its thunk on the decoded message, as before. Null when the + * signature's parameter has no message reader (messageReadable). */ + messageListenerHelper(t: IrType & { kind: "func" }): string | null { + const key = typeKey(t); + const existing = this.messageListeners.get(key); + if (existing !== undefined) return existing; + const param = t.params[0]; + const supported = + t.params.length === 1 && + t.rest !== true && + param !== undefined && + (t.ret.kind === "void" || t.ret.kind === "f64" || t.ret.kind === "bool") && + this.messageReadable(param, new Set()); + if (!supported) { + this.messageListeners.set(key, null); + return null; + } + const name = `sc_dml_${this.messageListeners.size}`; + this.messageListeners.set(key, name); + const B = new BlockBuilder(); + B.entryAllocas.push(`%fail = alloca i1`); + B.line(`store i1 false, ptr %fail`); + const value = B.tmp(); + B.line( + `${value} = call ${this.valTy(param)} @${this.messageReaderHelper(param)}(ptr %m, ${this.host.sizeType} 0, ptr %fail)`, + ); + const failed = B.tmp(); + B.line(`${failed} = load i1, ptr %fail`); + const lMiss = B.newLabel("dml.miss"); + const lCall = B.newLabel("dml.call"); + B.condBr(failed, lMiss, lCall); + B.startBlock(lMiss); + B.terminate(`ret i1 false`); + B.startBlock(lCall); + // The closure consumes its parameter; a pending exception stays for + // the emitter, as after the thunk. + const fnp = B.tmp(); + const fn = B.tmp(); + B.line(`${fnp} = getelementptr inbounds %ScrClosure, ptr %c, i64 0, i32 1`); + B.line(`${fn} = load ptr, ptr ${fnp}`); + const args = `ptr %c, ${this.valTy(param)} ${value}`; + if (t.ret.kind === "void") B.line(`call void ${fn}(${args})`); + else B.line(`${B.tmp()} = call ${this.valTy(t.ret)} ${fn}(${args})`); + B.terminate(`ret i1 true`); + this.defs.push( + `define internal zeroext i1 @${name}(ptr %c, ptr %m) ${FN_ATTRS} { ; message entry for ${llvmCommentText(key)}`, + B.render(), + `}`, + ``, + ); + return name; + } + + /** Whether a message reader can build T: numbers, booleans and strings; + * records of them with only declared fields, none of union type; and + * boxed unions of those arms and null or undefined, with at most one + * number, boolean and string arm. */ + private messageReadable(t: IrType, seen: Set): boolean { + switch (t.kind) { + case "f64": + case "bool": + case "string": + return true; + case "record": { + const shape = this.host.recordsById.get(t.shapeId); + if (!shape || shape.tuple || shape.indexValue || shape.fields.length === 0) return false; + if (seen.has(t.shapeId) || this.isRecursiveShape(t)) return false; + seen.add(t.shapeId); + const readable = shape.fields.every( + (f) => + !f.name.startsWith("%") && + f.type.kind !== "union" && + llFieldType(f.type, this.host.inlineUnions.record(f.type)) !== INLINE_UNION_FIELD && + this.messageReadable(f.type, seen), + ); + seen.delete(t.shapeId); + return readable; + } + case "union": { + const def = this.host.unionsById.get(t.unionId); + if (!def || this.host.nullableUnions.has(t.unionId)) return false; + const scalars = new Set(); + for (const arm of def.arms) { + if (arm.kind === "nullT" || arm.kind === "undefinedT") continue; + if (arm.kind === "f64" || arm.kind === "bool" || arm.kind === "string") { + if (scalars.has(arm.kind)) return false; + scalars.add(arm.kind); + continue; + } + if (arm.kind !== "record" || !this.messageReadable(arm, seen)) return false; + } + return true; + } + default: + return false; + } + } + + /** `sc_dmr_(ptr message, size node, ptr fail) -> T`: T (+1) built + * from a message node, for a type messageReadable accepts. Sets *fail + * and returns a dummy when the node does not have T's layout: the arm + * the checked conversion would choose, a record's declared keys in + * declaration order and no others. */ + private messageReaderHelper(t: IrType): string { + const key = typeKey(t); + const existing = this.messageReaders.get(key); + if (existing) return existing; + const name = `sc_dmr_${this.messageReaders.size}`; + this.messageReaders.set(key, name); + const host = this.host; + const S = host.sizeType; + if (t.kind !== "record") host.declare(`declare i32 @scr_message_kind(ptr, ${S})`); + const retTy = this.valTy(t); + const dummy = retTy === "double" ? f64Lit(0) : retTy === "i1" ? "false" : "null"; + const B = new BlockBuilder(); + const lFail = B.newLabel("dmr.fail"); + // scr_message_fields tests a record's kind itself. + const kind = B.tmp(); + if (t.kind !== "record") B.line(`${kind} = call i32 @scr_message_kind(ptr %m, ${S} %n)`); + const requireKind = (want: number): void => { + const ok = B.tmp(); + B.line(`${ok} = icmp eq i32 ${kind}, ${want}`); + const lOk = B.newLabel("dmr.kind"); + B.condBr(ok, lOk, lFail); + B.startBlock(lOk); + }; + switch (t.kind) { + case "f64": + case "bool": { + host.declare(`declare double @scr_message_number(ptr, ${S})`); + requireKind(t.kind === "f64" ? MESSAGE_KIND.NUMBER : MESSAGE_KIND.BOOL); + const n = B.tmp(); + B.line(`${n} = call double @scr_message_number(ptr %m, ${S} %n)`); + if (t.kind === "f64") B.terminate(`ret double ${n}`); + else { + const b = B.tmp(); + B.line(`${b} = fcmp une double ${n}, ${f64Lit(0)}`); + B.terminate(`ret i1 ${b}`); + } + break; + } + case "string": { + host.declare(`declare ptr @scr_message_string(ptr, ${S})`); + requireKind(MESSAGE_KIND.STRING); + const str = B.tmp(); + B.line(`${str} = call ptr @scr_message_string(ptr %m, ${S} %n)`); + B.terminate(`ret ptr ${str}`); + break; + } + case "record": { + const shape = host.recordsById.get(t.shapeId)!; + const order = recordSourceFieldOrder(shape); + const fields = new Map(shape.fields.map((f, i) => [f.name, { field: f, index: i }])); + const base = host.nullableUnions.fieldBase(t.shapeId); + const struct = mangleRecordStruct(t.shapeId); + const names = this.recordNameTable(t.shapeId, shape); + host.declare(`declare zeroext i1 @scr_message_fields(ptr, ${S}, ptr, ptr, ${S}, ptr)`); + B.entryAllocas.push(`%kids = alloca [${order.length} x ${S}]`); + const matched = B.tmp(); + B.line( + `${matched} = call zeroext i1 @scr_message_fields(ptr %m, ${S} %n, ptr ${names.names}, ptr ${names.lens}, ${S} ${order.length}, ptr %kids)`, + ); + const lBuild = B.newLabel("dmr.build"); + B.condBr(matched, lBuild, lFail); + B.startBlock(lBuild); + B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`); + order.forEach((fieldName, i) => { + const { field, index } = fields.get(fieldName)!; + const kidp = B.tmp(); + const kid = B.tmp(); + B.line( + `${kidp} = getelementptr inbounds [${order.length} x ${S}], ptr %kids, i64 0, i64 ${i}`, + ); + B.line(`${kid} = load ${S}, ptr ${kidp}`); + const v = B.tmp(); + B.line( + `${v} = call ${this.valTy(field.type)} @${this.messageReaderHelper(field.type)}(ptr %m, ${S} ${kid}, ptr %fail)`, + ); + const failed = B.tmp(); + B.line(`${failed} = load i1, ptr %fail`); + const lDrop = B.newLabel("dmr.drop"); + const lStore = B.newLabel("dmr.store"); + B.condBr(failed, lDrop, lStore); + B.startBlock(lDrop); + B.line(`call void ${releaseSym(host, t)}(ptr %r0)`); + B.terminate(`ret ptr null`); + B.startBlock(lStore); + const p = B.tmp(); + B.line( + `${p} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${index + base} ; .${llvmCommentText(fieldName)}`, + ); + const fieldTy = llFieldType(field.type, host.inlineUnions.record(field.type)); + if (fieldTy === "i8") { + const z = B.tmp(); + B.line(`${z} = zext i1 ${v} to i8`); + B.line(`store i8 ${z}, ptr ${p}`); + } else { + B.line(`store ${fieldTy} ${v}, ptr ${p}`); + } + }); + B.terminate(`ret ptr %r0`); + break; + } + case "union": { + const def = host.unionsById.get(t.unionId)!; + def.arms.forEach((arm, i) => { + const lNext = B.newLabel("dmr.next"); + if (arm.kind === "nullT" || arm.kind === "undefinedT") { + const hit = B.tmp(); + B.line( + `${hit} = icmp eq i32 ${kind}, ${arm.kind === "nullT" ? MESSAGE_KIND.NULL : MESSAGE_KIND.UNDEFINED}`, + ); + const lHit = B.newLabel("dmr.unit"); + B.condBr(hit, lHit, lNext); + B.startBlock(lHit); + B.terminate(`ret ptr ${host.unitInstanceRef(t.unionId, i)}`); + B.startBlock(lNext); + return; + } + if (arm.kind !== "record") { + const want = + arm.kind === "f64" + ? MESSAGE_KIND.NUMBER + : arm.kind === "bool" + ? MESSAGE_KIND.BOOL + : MESSAGE_KIND.STRING; + const hit = B.tmp(); + B.line(`${hit} = icmp eq i32 ${kind}, ${want}`); + const lHit = B.newLabel("dmr.arm"); + B.condBr(hit, lHit, lNext); + B.startBlock(lHit); + } + // A record arm that does not match leaves *fail clear for the + // next arm, like the checked conversion's next-arm match. + const v = B.tmp(); + B.line( + `${v} = call ${this.valTy(arm)} @${this.messageReaderHelper(arm)}(ptr %m, ${S} %n, ptr %fail)`, + ); + if (arm.kind === "record") { + const failed = B.tmp(); + B.line(`${failed} = load i1, ptr %fail`); + const lHit = B.newLabel("dmr.arm"); + const lMiss = B.newLabel("dmr.miss"); + B.condBr(failed, lMiss, lHit); + B.startBlock(lMiss); + B.line(`store i1 false, ptr %fail`); + B.br(lNext); + B.startBlock(lHit); + } + const u = B.tmp(); + if (arm.kind === "f64") { + host.declare(`declare ptr @scr_union_new_f64(i32, double)`); + B.line(`${u} = call ptr @scr_union_new_f64(i32 ${i}, double ${v})`); + } else if (arm.kind === "bool") { + host.declare(`declare ptr @scr_union_new_bool(i32, i1 zeroext)`); + B.line(`${u} = call ptr @scr_union_new_bool(i32 ${i}, i1 ${v})`); + } else { + const rc = vAdapters(host, arm); + host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`); + B.line( + `${u} = call ptr @scr_union_new_ref(i32 ${i}, ptr ${v}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, arm)})`, + ); + } + B.terminate(`ret ptr ${u}`); + B.startBlock(lNext); + }); + B.br(lFail); + break; + } + default: + throw new InternalCompilerError(`llvm emitter bug: message reader of ${key}`); + } + B.startBlock(lFail); + B.line(`store i1 true, ptr %fail`); + B.terminate(`ret ${retTy} ${dummy}`); + this.defs.push( + `define internal ${retTy === "i1" ? "zeroext i1" : retTy} @${name}(ptr %m, ${S} %n, ptr %fail) ${FN_ATTRS} { ; message reader for ${llvmCommentText(key)}`, + B.render(), + `}`, + ``, + ); + return name; + } + /** The adapter closure body for one TARGET signature: caps[0] is an * untraced obj-box owning the dyn function value. */ dynFuncAdapterHelper(t: IrType & { kind: "func" }): string { diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 563a900f8f..58bb905695 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -16,6 +16,7 @@ import { streamTypedRefEligible } from "../../ir/analysis.js"; import { DYN, type IrExpr, + type IrType, isDynTypedRefType, isRefCounted, isUnitType, @@ -221,6 +222,19 @@ export function emitDynamicExpr( host.own({ name: prepared, type: { kind: "dyn" } }); host.emitPendingCheck(); const args = e.args.map((a) => emitBorrowedInput(host, a)); + // A closure registered as a 'message' listener gets a typed message + // entry: ports deliver messages of its parameter's layout without + // decoding them to checked-dynamic values first. + const listener = messageListenerArg(e); + if (listener !== null) { + const entry = host.dyn.messageListenerHelper(listener); + if (entry !== null) { + host.declare(`declare void @scr_dyn_func_set_message(ptr, ptr, ptr)`); + B.line( + `call void @scr_dyn_func_set_message(ptr ${args[1]!.name}, ptr @${host.dyn.dynFuncThunkHelper(listener)}, ptr @${entry})`, + ); + } + } let argsPtr = "null"; if (args.length > 0) { const arr = B.slot(); @@ -1323,3 +1337,24 @@ export const INHERITED_OBJECT_MEMBERS: ReadonlySet = new Set([ "hasOwnProperty", "propertyIsEnumerable", ]); + +/** EventEmitter methods that register their second argument as a listener. */ +const LISTENER_METHODS: ReadonlySet = new Set([ + "on", + "once", + "addListener", + "prependListener", + "prependOnceListener", +]); + +/** The closure type of `recv.on("message", listener)` and the other + * listener registrations with a typed closure for the 'message' event, or + * null. */ +function messageListenerArg(e: ExprOf<"dynInvoke">): (IrType & { kind: "func" }) | null { + if (!LISTENER_METHODS.has(e.method) || e.args.length !== 2) return null; + const [event, listener] = e.args; + if (event?.kind !== "dynFrom" || event.value.kind !== "strLit" || event.value.value !== "message") + return null; + if (listener?.kind !== "dynFrom" || listener.value.type.kind !== "func") return null; + return listener.value.type; +} diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 703e8fe48f..3a41d91769 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 c884f11324..af61e52290 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 3a0b7870a5..d095099114 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/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index af8d9b7245..35a747d603 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/7130-typed-message-listeners.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7130-typed-message-listeners.ts.json new file mode 100644 index 0000000000..f022443247 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7130-typed-message-listeners.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7130-typed-message-listeners.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7131-typed-worker-messages.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7131-typed-worker-messages.ts.json new file mode 100644 index 0000000000..89a6c125a0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7131-typed-worker-messages.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7131-typed-worker-messages.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 012b860130..631750cdf9 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 5b8f3563f1..0e81033ad7 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 24cd8093b4..a3ac43cc01 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 791c600c7c..fea020d164 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.h b/packages/runtime/src/scr_dyn.h index c9d090370e..2602e0341f 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1,6 +1,7 @@ /* scriptc runtime — dynamic values: the checked-dynamic tree, native handles, * dyn promises, the ambient receiver and listener adapters (scr_json.c, - * scr_dyn_handle.c, scr_async_dyn.c). Included by scr_runtime.h, which says + * scr_dyn_handle.c, scr_async_dyn.c), and the message readers of typed + * 'message' listeners (scr_message.c). Included by scr_runtime.h, which says * where new declarations go. */ #ifndef SCR_DYN_H #define SCR_DYN_H @@ -254,6 +255,55 @@ typedef struct ScrDynTypedCast { * result as an owned (+1) dyn value. `args` entries are BORROWED. */ typedef ScrDyn *(*ScrDynThunk)(ScrClosure *clo, ScrDyn *const *args, size_t argc); +/* A compiled 'message' listener's typed entry: builds the closure's + * parameter from the message's nodes (the readers below) and calls + * the closure. Answers false, without calling it, when the message does + * not have the parameter's layout; the caller then decodes the message + * and calls the thunk. */ +struct ScrMessage; +typedef bool (*ScrDynMessageListener)(ScrClosure *clo, const struct ScrMessage *message); + +/* The kinds of a message's nodes (scr_message.c). */ +typedef enum { + SCR_MESSAGE_UNDEFINED, + SCR_MESSAGE_NULL, + SCR_MESSAGE_BOOL, + SCR_MESSAGE_NUMBER, + SCR_MESSAGE_STRING, + SCR_MESSAGE_BIGINT, + SCR_MESSAGE_ARRAY, + SCR_MESSAGE_OBJECT, + SCR_MESSAGE_STORAGE, + SCR_MESSAGE_BYTES, + SCR_MESSAGE_ARRAY_BUFFER, + SCR_MESSAGE_ERROR, + /* A deeply frozen plain-data graph shared by pointer (see publication). */ + SCR_MESSAGE_PUBLISHED, + /* A typed value published with @scriptc/threads (immortal), delivered as + * a fresh typed-ref capsule over the same pointer. */ + SCR_MESSAGE_TYPED_REF, + /* A transferred MessagePort: its channel end (+1) and side. */ + SCR_MESSAGE_PORT, + /* A native handle cloned through its unit's ScrDynHandleTransfer. */ + SCR_MESSAGE_HANDLE, +} ScrMessageKind; + +/* Readers for compiled 'message' listeners (ScrDynMessageListener), which + * build their typed parameter from the nodes. Node 0 is the root, and a + * node's values have higher numbers. scr_message_readable answers whether + * the message can reach these listeners: no node is reached twice, and it + * holds no ports or native handles, which the receiver adopts when it + * decodes the message. kind answers a ScrMessageKind; number reads a + * NUMBER or BOOL node, string a STRING node (+1). fields answers whether an + * OBJECT node has exactly the given keys, in order, and writes their value + * nodes. */ +bool scr_message_readable(const struct ScrMessage *message); +int32_t scr_message_kind(const struct ScrMessage *message, size_t node); +double scr_message_number(const struct ScrMessage *message, size_t node); +ScrStr *scr_message_string(const struct ScrMessage *message, size_t node); +bool scr_message_fields(const struct ScrMessage *message, size_t node, const char *const *names, + const size_t *lengths, size_t count, size_t *values); + /* Object member. Keys own shared immutable UTF-8 bytes (NUL-terminated for * convenience; key_len excludes the NUL) — duplicate keys were already * collapsed at parse time (later wins, like JS JSON.parse). */ @@ -391,7 +441,7 @@ struct ScrDyn { * and `name` are static compiler-emitted literals (never freed); name * may be NULL (anonymous — inspect prints [Function (anonymous)]). * The dyn→closure ownership edge is traced by the cycle collector. */ - struct { ScrClosure *clo; ScrDynThunk thunk; const char *sig; const char *name; uint32_t arity; ScrClassObj *class_obj; } fn; + struct { ScrClosure *clo; ScrDynThunk thunk; const char *sig; const char *name; uint32_t arity; ScrClassObj *class_obj; ScrDynMessageListener message; } fn; /* Native handles normally keep conservative ownership edges. Sets * backed by cycle-capable maps expose their edge to the collector. * Cache that allocation property: during collection the map may be @@ -1246,6 +1296,9 @@ ScrDyn *scr_dyn_define_prototype_props(ScrDyn *target, ScrDyn *descs, ScrDyn *pr * (callers retain first when they keep their own reference); `sig`/`name` * must be static literals (the box never frees them; name may be NULL). */ ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, const char *sig, const char *name); +/* Gives a function box made with thunk its typed message entry (an + * EventEmitter listener registered from it keeps the entry). */ +void scr_dyn_func_set_message(ScrDyn *fn, ScrDynThunk thunk, ScrDynMessageListener listener); ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key); /* EventEmitter and the stream constructors as untyped constructor values, * and instanceof over untyped constructor functions diff --git a/packages/runtime/src/scr_events_emitter.c b/packages/runtime/src/scr_events_emitter.c index 901e2d1141..5e03c0d6ce 100644 --- a/packages/runtime/src/scr_events_emitter.c +++ b/packages/runtime/src/scr_events_emitter.c @@ -82,6 +82,8 @@ typedef struct ScrEeEntry { * the meta events. */ ScrClosure *flex_cb; ScrDynThunk flex_invoke; + /* The function box's typed message entry (scr_emitter_emit_message). */ + ScrDynMessageListener flex_message; const char *flex_sig; const char *flex_name; uint32_t flex_arity; @@ -512,6 +514,7 @@ static ScrEmitter *scr_ee_add(ScrEmitter *em, ScrStr *name, ScrClosure *cb /*mov if (dyn) { e->flex_cb = scr_closure_retain(dyn->v.fn.clo); e->flex_invoke = dyn->v.fn.thunk; + e->flex_message = dyn->v.fn.message; e->flex_sig = dyn->v.fn.sig; e->flex_name = dyn->v.fn.name; e->flex_arity = dyn->v.fn.arity; @@ -1013,15 +1016,16 @@ static ScrDyn *const scr_ee_no_args[1] = { NULL }; * a nested emit's snapshot still holds it (the shared fired flag — Node's * wrapper.fired). A listener throw stops the pass (the exception stays * pending for the caller). Returns whether the bucket had listeners. */ +typedef struct ScrEeMessage ScrEeMessage; static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template, - ScrDyn *const *flex_args, size_t flex_argc); + ScrDyn *const *flex_args, size_t flex_argc, ScrEeMessage *message); /* emit('error', ...) first emits the same arguments to errorMonitor. */ static void scr_ee_emit_error_monitor(ScrEmitter *em, va_list *ap_template, ScrDyn *const *flex_args, size_t flex_argc) { ScrEeBucket *b = scr_ee_bucket_of(em->reg, SCR_EE_ERROR_MONITOR); if (!b || b->n == 0) return; - scr_ee_emit_core(em, SCR_EE_ERROR_MONITOR, ap_template, flex_args, flex_argc); + scr_ee_emit_core(em, SCR_EE_ERROR_MONITOR, ap_template, flex_args, flex_argc, NULL); } /* A listener this emit has no argument form for: an untyped emit whose @@ -1044,8 +1048,17 @@ SCR_COLD static void scr_ee_listener_mismatch(const ScrStr *name, bool typed_emi scr_throw_error(typed_emit ? SCR_ERR_ERROR : SCR_ERR_TYPE, message); } +/* A message emit (scr_emitter_emit_message): listeners with a typed message + * entry read the message; the others share one decoded argument, made when + * the first of them runs. */ +struct ScrEeMessage { + const struct ScrMessage *message; + ScrDyn *(*decode)(const struct ScrMessage *); + ScrDyn *decoded; +}; + static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template, - ScrDyn *const *flex_args, size_t flex_argc) { + ScrDyn *const *flex_args, size_t flex_argc, ScrEeMessage *message) { if (name->len == 5 && memcmp(name->data, "error", 5) == 0) { scr_ee_emit_error_monitor(em, ap_template, flex_args, flex_argc); if (scr_exc_pending()) return true; @@ -1088,6 +1101,19 @@ static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template, continue; } } + if (message && e->flex_message && e->flex_message(e->flex_cb, message->message)) { + scr_ee_entry_unref(e); + continue; + } + if (message && !message->decoded) { + message->decoded = message->decode(message->message); + if (!message->decoded) { + scr_ee_entry_unref(e); + continue; + } + flex_args = &message->decoded; + flex_argc = 1; + } if (flex_args && e->flex_invoke) { ScrDyn *answer = e->flex_invoke(e->flex_cb, flex_args, flex_argc); scr_dyn_release(answer); /* EventEmitter ignores callback results. */ @@ -1108,7 +1134,7 @@ static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template, static bool scr_ee_emit_va(ScrEmitter *em, ScrStr *name, ...) { va_list ap; va_start(ap, name); - bool had = scr_ee_emit_core(em, name, &ap, NULL, 0); + bool had = scr_ee_emit_core(em, name, &ap, NULL, 0, NULL); va_end(ap); return had; } @@ -1119,7 +1145,7 @@ static bool scr_ee_emit_mixed_va(ScrEmitter *em, ScrStr *name, ScrDyn *const *ar int typed, ...) { va_list ap; va_start(ap, typed); - bool had = scr_ee_emit_core(em, name, typed ? &ap : NULL, args ? args : scr_ee_no_args, argc); + bool had = scr_ee_emit_core(em, name, typed ? &ap : NULL, args ? args : scr_ee_no_args, argc, NULL); va_end(ap); return had; } @@ -1131,7 +1157,7 @@ static bool scr_ee_emit_mixed_va(ScrEmitter *em, ScrStr *name, ScrDyn *const *ar bool scr_emitter_emit(ScrEmitter *em, ScrStr *name, ...) { va_list ap; va_start(ap, name); - bool had = scr_ee_emit_core(em, name, &ap, NULL, 0); + bool had = scr_ee_emit_core(em, name, &ap, NULL, 0, NULL); va_end(ap); return had; } @@ -1143,7 +1169,7 @@ bool scr_emitter_emit(ScrEmitter *em, ScrStr *name, ...) { bool scr_emitter_emit_mixed(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc, ...) { va_list ap; va_start(ap, argc); - bool had = scr_ee_emit_core(em, name, &ap, args ? args : scr_ee_no_args, argc); + bool had = scr_ee_emit_core(em, name, &ap, args ? args : scr_ee_no_args, argc, NULL); va_end(ap); return had; } @@ -1241,7 +1267,15 @@ static bool scr_ee_emit_error_dyn(ScrEmitter *em, ScrStr *name, ScrDyn *const *a /* An untyped emit of a name typed code never fixes a tuple for (computed * names, runtime-internal registries): the arguments as given. */ bool scr_emitter_emit_flex(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc) { - return scr_ee_emit_core(em, name, NULL, args ? args : scr_ee_no_args, argc); + return scr_ee_emit_core(em, name, NULL, args ? args : scr_ee_no_args, argc, NULL); +} + +bool scr_emitter_emit_message(ScrEmitter *em, ScrStr *name, const struct ScrMessage *message, + ScrDyn *(*decode)(const struct ScrMessage *)) { + ScrEeMessage state = {message, decode, NULL}; + bool had = scr_ee_emit_core(em, name, NULL, NULL, 0, &state); + scr_dyn_release(state.decoded); + return had; } /* emit(name, ...args) through an untyped value: the arguments as given. @@ -1255,7 +1289,7 @@ bool scr_emitter_emit_any(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, siz return scr_ee_emit_mixed_va(em, name, args, argc, s != NULL, s); } } - return scr_ee_emit_core(em, name, NULL, args ? args : scr_ee_no_args, argc); + return scr_ee_emit_core(em, name, NULL, args ? args : scr_ee_no_args, argc, NULL); } /* Handle event registries use checked callbacks for every tuple, including @@ -1268,7 +1302,7 @@ bool scr_emitter_emit_error_flex(ScrEmitter *em, ScrDyn *error) { return false; } ScrStr *name = scr_str_new("error", 5); - bool had = scr_ee_emit_core(em, name, NULL, &error, 1); + bool had = scr_ee_emit_core(em, name, NULL, &error, 1, NULL); scr_str_release(name); return had; } diff --git a/packages/runtime/src/scr_events_emitter.h b/packages/runtime/src/scr_events_emitter.h index b66de9e5a4..6e1ef536a5 100644 --- a/packages/runtime/src/scr_events_emitter.h +++ b/packages/runtime/src/scr_events_emitter.h @@ -154,6 +154,12 @@ ScrEmitter *scr_emitter_remove_all(ScrEmitter *em, ScrStr *name, bool all); * emit('error', err): no listener ⇒ THROWS err (borrowed; +1 taken). */ bool scr_emitter_emit(ScrEmitter *em, ScrStr *name, ...); bool scr_emitter_emit_flex(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc); +/* An untyped emit of one message: a listener registered from a function + * box with a typed message entry (scr_dyn_func_set_message) reads the + * message itself; the other listeners get decode(message), made once. */ +struct ScrMessage; +bool scr_emitter_emit_message(ScrEmitter *em, ScrStr *name, const struct ScrMessage *message, + ScrDyn *(*decode)(const struct ScrMessage *)); /* An untyped emit of an event with a fixed tuple: the arguments as given * for dynamic listeners, plus the tuple converted by the caller as * scr_emitter_emit's variadics for typed listeners. has_typed answers diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index f610ffdbb8..355a34aee0 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4122,9 +4122,16 @@ ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, con /* A signature adapter reports the length of the function it adapts. */ d->v.fn.arity = clo->identity != NULL ? clo->identity_length : arity; d->v.fn.class_obj = NULL; + d->v.fn.message = NULL; return d; } +void scr_dyn_func_set_message(ScrDyn *fn, ScrDynThunk thunk, ScrDynMessageListener listener) { + /* scr_dyn_new_func answers an adapted function's original box, whose + * closure has other parameter types. */ + if (fn && fn->kind == SCR_DYN_FUNC && fn->v.fn.thunk == thunk) fn->v.fn.message = listener; +} + /* A class is function-shaped for typeof and identity, but calling it without * new always throws. The closure owns the class object and its lexical boxes. */ static ScrDyn *scr_dyn_class_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index 5e1bce37f0..5082661035 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -3,30 +3,6 @@ #include #include -typedef enum { - SCR_MESSAGE_UNDEFINED, - SCR_MESSAGE_NULL, - SCR_MESSAGE_BOOL, - SCR_MESSAGE_NUMBER, - SCR_MESSAGE_STRING, - SCR_MESSAGE_BIGINT, - SCR_MESSAGE_ARRAY, - SCR_MESSAGE_OBJECT, - SCR_MESSAGE_STORAGE, - SCR_MESSAGE_BYTES, - SCR_MESSAGE_ARRAY_BUFFER, - SCR_MESSAGE_ERROR, - /* A deeply frozen plain-data graph shared by pointer (see publication). */ - SCR_MESSAGE_PUBLISHED, - /* A typed value published with @scriptc/threads (immortal), delivered as - * a fresh typed-ref capsule over the same pointer. */ - SCR_MESSAGE_TYPED_REF, - /* A transferred MessagePort: its channel end (+1) and side. */ - SCR_MESSAGE_PORT, - /* A native handle cloned through its unit's ScrDynHandleTransfer. */ - SCR_MESSAGE_HANDLE, -} ScrMessageKind; - const ScrMessagePortOps *scr_message_port_ops = NULL; typedef struct { @@ -95,6 +71,7 @@ struct ScrMessage { size_t count; bool storage; /* has STORAGE nodes */ bool releases; /* has STORAGE, PORT or HANDLE nodes, which own resources */ + bool readable; /* see scr_message_readable */ }; typedef struct { @@ -143,6 +120,8 @@ typedef struct { bool uncaught; /* the next value is an uncaught exception or its cause */ bool storage; bool releases; + bool shared; /* a node was reached twice */ + bool adopts; /* has PORT or HANDLE nodes */ ScrMessageNode inline_nodes[SCR_MESSAGE_INLINE]; ScrMessageVisit inline_visits[SCR_MESSAGE_INLINE]; ScrMessagePending inline_properties[SCR_MESSAGE_INLINE]; @@ -168,7 +147,7 @@ static void scr_message_encoder_init(ScrMessageEncoder *encoder, const ScrDyn *t encoder->stack_capacity = SCR_MESSAGE_INLINE; encoder->transfer = transfer; encoder->uncaught = uncaught; - encoder->storage = encoder->releases = false; + encoder->storage = encoder->releases = encoder->shared = encoder->adopts = false; } static bool scr_message_listed(const ScrMessageEncoder *encoder, const ScrDyn *value) { @@ -290,7 +269,10 @@ static void scr_message_push(ScrMessageEncoder *encoder, size_t node) { static size_t scr_message_storage(ScrMessageEncoder *encoder, const ScrBytes *bytes) { const ScrBytes *owner = bytes->backing ? bytes->backing : bytes; size_t found = scr_message_find(encoder, owner, SCR_MESSAGE_STORAGE + 1); - if (found != SIZE_MAX) return found; + if (found != SIZE_MAX) { + encoder->shared = true; + return found; + } size_t node = scr_message_node(encoder); scr_message_identify(encoder, owner, SCR_MESSAGE_STORAGE + 1, node); ScrMessageNode *storage = &encoder->nodes[node]; @@ -360,7 +342,10 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) return SIZE_MAX; } size_t found = scr_message_find(encoder, identity, domain); - if (found != SIZE_MAX) return found; + if (found != SIZE_MAX) { + encoder->shared = true; + return found; + } size_t id = scr_message_node(encoder); scr_message_identify(encoder, identity, domain, id); encoder->visits[id].input = scr_dyn_retain((ScrDyn *)input); @@ -482,7 +467,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) node->kind = SCR_MESSAGE_PORT; node->v.port.channel = channel; node->v.port.side = side; - encoder->releases = true; + encoder->releases = encoder->adopts = true; break; } bool buffer = scr_buffer_storage_is(value); @@ -498,7 +483,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) node->kind = SCR_MESSAGE_HANDLE; node->v.handle.transfer = transfer; node->v.handle.payload = payload; - encoder->releases = true; + encoder->releases = encoder->adopts = true; break; } const ScrBytes *bytes = buffer ? value->v.handle.ptr : value->v.bytes; @@ -678,6 +663,7 @@ static ScrMessage *scr_message_try_publish(const ScrDyn *value) { message = scr_message_alloc(1, sizeof(*message) + sizeof(*message->nodes)); message->nodes = (ScrMessageNode *)(message + 1); message->count = 1; + message->readable = true; message->nodes[0].kind = SCR_MESSAGE_PUBLISHED; message->nodes[0].v.published = (ScrDyn *)value; if (getenv("SCRIPTC_PUBLISH_TRACE")) @@ -770,7 +756,8 @@ static ScrMessage *scr_message_pack(ScrMessageEncoder *encoder) { ScrMessageProperty *slots = (ScrMessageProperty *)(nodes + count); ScrMessageTyped *records = (ScrMessageTyped *)(slots + properties); char *text = (char *)(records + typed); - *message = (ScrMessage){nodes, count, encoder->storage, encoder->releases}; + *message = (ScrMessage){nodes, count, encoder->storage, encoder->releases, + !encoder->shared && !encoder->adopts}; memcpy(nodes, encoder->nodes, count * sizeof(*nodes)); if (typed) memcpy(records, encoder->typed, typed * sizeof(*records)); size_t first = 0; @@ -1014,6 +1001,28 @@ done:; return result; } +bool scr_message_readable(const ScrMessage *message) { return message->readable; } + +int32_t scr_message_kind(const ScrMessage *message, size_t node) { return message->nodes[node].kind; } + +double scr_message_number(const ScrMessage *message, size_t node) { return message->nodes[node].v.number; } + +ScrStr *scr_message_string(const ScrMessage *message, size_t node) { + return scr_str_new(message->nodes[node].v.text.data, message->nodes[node].v.text.length); +} + +bool scr_message_fields(const ScrMessage *message, size_t node, const char *const *names, + const size_t *lengths, size_t count, size_t *values) { + const ScrMessageNode *object = &message->nodes[node]; + if (object->kind != SCR_MESSAGE_OBJECT || object->count != count) return false; + for (size_t i = 0; i < count; i++) { + const ScrMessageProperty *property = &object->properties[i]; + if (property->length != lengths[i] || memcmp(property->key, names[i], lengths[i])) return false; + values[i] = property->value; + } + return true; +} + void scr_message_free(ScrMessage *message) { if (!message) return; if (message->releases) diff --git a/packages/runtime/src/scr_message.test.c b/packages/runtime/src/scr_message.test.c index bdabc4d12d..b6adf34c5b 100644 --- a/packages/runtime/src/scr_message.test.c +++ b/packages/runtime/src/scr_message.test.c @@ -250,6 +250,52 @@ static void check_large(void) { scr_message_free(message); } +/* The readers compiled 'message' listeners use: node kinds and values, a + * record's keys in order, and which messages they may read. */ +static void check_readers(void) { + ScrDyn *root = scr_dyn_new_obj(); + put_number(root, "id", 7); + ScrStr *text = scr_str_new("seven", 5); + scr_dyn_obj_set(root, "name", 4, scr_dyn_new_str(text)); + scr_str_release(text); + scr_dyn_obj_set(root, "done", 4, scr_dyn_new_bool(true)); + ScrMessage *message = scr_message_encode(root); + assert(message && scr_message_readable(message)); + assert(scr_message_kind(message, 0) == SCR_MESSAGE_OBJECT); + static const char *const names[] = {"id", "name", "done"}; + static const size_t lengths[] = {2, 4, 4}; + size_t values[3]; + assert(scr_message_fields(message, 0, names, lengths, 3, values)); + assert(scr_message_kind(message, values[0]) == SCR_MESSAGE_NUMBER); + assert(scr_message_number(message, values[0]) == 7); + assert(scr_message_kind(message, values[1]) == SCR_MESSAGE_STRING); + ScrStr *name = scr_message_string(message, values[1]); + assert(name->len == 5 && !memcmp(name->data, "seven", 5)); + scr_str_release(name); + assert(scr_message_kind(message, values[2]) == SCR_MESSAGE_BOOL); + assert(scr_message_number(message, values[2]) == 1); + /* Another order, a missing key or an extra key does not match. */ + static const char *const reordered[] = {"name", "id", "done"}; + static const size_t reordered_lengths[] = {4, 2, 4}; + assert(!scr_message_fields(message, 0, reordered, reordered_lengths, 3, values)); + assert(!scr_message_fields(message, 0, names, lengths, 2, values)); + assert(!scr_message_fields(message, values[0], names, lengths, 3, values)); + scr_message_free(message); + put_number(root, "extra", 1); + message = scr_message_encode(root); + assert(!scr_message_fields(message, 0, names, lengths, 3, values)); + scr_message_free(message); + /* A value reached twice makes the message unreadable. */ + ScrDyn *shared = scr_dyn_new_obj(); + scr_dyn_obj_set(root, "first", 5, scr_dyn_retain(shared)); + scr_dyn_obj_set(root, "second", 6, shared); + message = scr_message_encode(root); + assert(message && !scr_message_readable(message)); + scr_message_free(message); + scr_dyn_release(root); + scr_collect_cycles(); +} + /* A link-gated unit's handle crosses threads through the transfer hooks in * its ops: share on the sender, adopt on the receiver, drop with the * message. The test borrows the last capacity slot, which no unit uses, so @@ -376,6 +422,7 @@ int main(void) { check_rejection(); check_documents(); check_large(); + check_readers(); check_handle_transfer(); scr_context_cleanup(); puts("message graphs preserve identity, sparse values, byte aliases and getter order"); diff --git a/packages/runtime/src/scr_worker_events.c b/packages/runtime/src/scr_worker_events.c index 252230372f..3d10183f48 100644 --- a/packages/runtime/src/scr_worker_events.c +++ b/packages/runtime/src/scr_worker_events.c @@ -665,6 +665,17 @@ static bool scr_channel_dispatch(void) { continue; } scr_worker_view_retain(view); + if (event->message && !view->broadcast && scr_message_readable(event->message)) { + /* Compiled listeners with a typed message entry read the message. */ + ScrStr *name = scr_str_new("message", 7); + scr_dyn_this_push(view, SCR_DYNH_MESSAGE_PORT); + scr_emitter_emit_message(view->events, name, event->message, scr_message_decode); + scr_dyn_this_pop(); + scr_str_release(name); + scr_mail_event_free(event); + scr_worker_view_release(view); + return true; + } ScrDyn *argument = event->message ? scr_message_decode(event->message) : NULL; if (argument && view->broadcast) { ScrDyn *self = scr_dyn_new_handle(view, SCR_DYNH_BROADCAST_CHANNEL); @@ -815,6 +826,19 @@ static void scr_worker_dispatch(void) { } } scr_worker_view_retain(view); + if (event->kind == SCR_WORKER_MESSAGE && event->message && scr_message_readable(event->message)) { + /* Compiled listeners with a typed message entry read the message. */ + if (!scr_exc_pending()) { + ScrStr *name = scr_str_new("message", 7); + scr_dyn_this_push(view, view->port ? SCR_DYNH_MESSAGE_PORT : SCR_DYNH_WORKER); + scr_emitter_emit_message(view->events, name, event->message, scr_message_decode); + scr_dyn_this_pop(); + scr_str_release(name); + } + scr_worker_view_release(view); + scr_mail_event_free(event); + return; + } const char *event_name = event->kind == SCR_WORKER_ONLINE ? "online" : event->kind == SCR_WORKER_EXIT ? "exit" : event->kind == SCR_WORKER_ERROR ? "error" : "message"; ScrDyn *argument = event->message ? scr_message_decode(event->message) : diff --git a/tests/corpus/7130-typed-message-listeners.ts b/tests/corpus/7130-typed-message-listeners.ts new file mode 100644 index 0000000000..b98983824a --- /dev/null +++ b/tests/corpus/7130-typed-message-listeners.ts @@ -0,0 +1,80 @@ +import { MessageChannel } from "node:worker_threads"; + +// 'message' listeners with typed parameters on MessageChannel ports. A +// message with the parameter's exact layout, and one with other keys, +// shared values or other kinds, reach the listener with the same values as +// in Node. +type Item = { id: number; name: string; done: boolean }; +type Nested = { label: string; item: Item }; + +const { port1, port2 } = new MessageChannel(); +// Each port's lines print in delivery order once every message arrived. +const lines: string[] = []; +const nestedLines: string[] = []; +const otherLines: string[] = []; +let pending = 0; +function finish(): void { + if (--pending === 0) { + for (const line of [...lines, ...nestedLines, ...otherLines]) console.log(line); + port1.close(); + channel.port1.close(); + other.port1.close(); + } +} + +port1.on("message", (value: number | string | Item | null | undefined) => { + if (value === null || value === undefined) lines.push(`unit ${String(value)}`); + else if (typeof value === "number") lines.push(`number ${value.toFixed(2)}`); + else if (typeof value === "string") lines.push(`string ${value.length} ${value}`); + else lines.push(`item ${value.id} ${value.name} ${value.done} ${JSON.stringify(value)}`); + finish(); +}); + +const sent: unknown[] = [ + 1.5, + -0, + "", + "héllo wörld", + null, + undefined, + { id: 1, name: "first", done: false }, + { id: 2, name: "", done: true }, + // An extra key stays beside the record. + { id: 4, name: "extra", done: false, note: "kept" }, +]; +pending += sent.length; +for (const value of sent) port2.postMessage(value); + +// Nested records, a value shared by two fields, and a second listener +// that reads the message untyped. +const channel = new MessageChannel(); +channel.port1.on("message", (value: Nested) => { + nestedLines.push(`nested ${value.label} ${value.item.id} ${value.item.name} ${JSON.stringify(value)}`); + finish(); +}); +channel.port1.on("message", (value: unknown) => { + nestedLines.push(`untyped ${JSON.stringify(value)}`); + finish(); +}); +const shared = { id: 5, name: "shared", done: true }; +const nested: unknown[] = [ + { label: "plain", item: { id: 6, name: "six", done: false } }, + { label: "shared", item: shared, again: shared }, +]; +pending += 2 * nested.length; +for (const value of nested) channel.port2.postMessage(value); + +// once listeners, and a listener added while the port delivers. +const other = new MessageChannel(); +other.port1.once("message", (value: Item) => { + otherLines.push(`once ${value.id} ${value.name}`); + other.port1.on("message", (later: number) => { + otherLines.push(`later ${later}`); + finish(); + }); + finish(); +}); +pending += 3; +other.port2.postMessage({ id: 7, name: "seven", done: true }); +other.port2.postMessage(8); +other.port2.postMessage(9); diff --git a/tests/corpus/7131-typed-worker-messages.ts b/tests/corpus/7131-typed-worker-messages.ts new file mode 100644 index 0000000000..86197b4dea --- /dev/null +++ b/tests/corpus/7131-typed-worker-messages.ts @@ -0,0 +1,54 @@ +import { MessageChannel, MessagePort, Worker, isMainThread, parentPort } from "node:worker_threads"; + +// 'message' listeners with typed parameters on a worker and its +// parentPort: records and numbers in both directions, and a message that +// carries a transferred MessagePort. +type Job = { id: number; text: string; urgent: boolean }; +type Reply = { id: number; length: number; upper: string }; + +if (isMainThread) { + const worker = new Worker(new URL(import.meta.url)); + // The worker's replies, then the port's, each in delivery order. + const replies: string[] = []; + const portLines: string[] = []; + let expected = 5; + const done = (): void => { + if (--expected > 0) return; + for (const line of [...replies, ...portLines]) console.log(line); + void worker.terminate(); + }; + worker.on("message", (reply: Reply | number) => { + if (typeof reply === "number") replies.push(`count ${reply}`); + else replies.push(`reply ${reply.id} ${reply.length} ${reply.upper}`); + done(); + }); + const jobs: Job[] = [ + { id: 1, text: "alpha", urgent: false }, + { id: 2, text: "βeta", urgent: true }, + { id: 3, text: "", urgent: false }, + ]; + for (const job of jobs) worker.postMessage(job); + worker.postMessage(jobs.length); + // A port in the message: the worker answers through it. + const { port1, port2 } = new MessageChannel(); + port1.on("message", (text: string) => { + portLines.push(`port ${text}`); + port1.close(); + done(); + }); + worker.postMessage({ port: port2 }, [port2]); +} else { + let count = 0; + parentPort!.on("message", (message: Job | number | { port: MessagePort }) => { + if (typeof message === "number") { + parentPort!.postMessage(count + message); + } else if ("port" in message) { + message.port.postMessage("through the port"); + message.port.close(); + } else { + count++; + const text = message.urgent ? message.text.toUpperCase() : message.text; + parentPort!.postMessage({ id: message.id, length: message.text.length, upper: text }); + } + }); +} diff --git a/tests/harness/corpus-costs/7130-typed-message-listeners.ts.json b/tests/harness/corpus-costs/7130-typed-message-listeners.ts.json new file mode 100644 index 0000000000..bf7b0d199a --- /dev/null +++ b/tests/harness/corpus-costs/7130-typed-message-listeners.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 2200 } diff --git a/tests/harness/corpus-costs/7131-typed-worker-messages.ts.json b/tests/harness/corpus-costs/7131-typed-worker-messages.ts.json new file mode 100644 index 0000000000..953e2af8fb --- /dev/null +++ b/tests/harness/corpus-costs/7131-typed-worker-messages.ts.json @@ -0,0 +1 @@ +{ "plain": 1700, "san": 1600 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 176caa5161..c0176eb93b 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 87162a491d4b0f4819bc6577f0045f02a3774912 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 07:20:14 -0500 Subject: [PATCH 131/395] Index dispatch receivers by name instead of testing each one ClassDynamicDispatch.process tested every property receiver for every property dispatch and for every method name a dynamic call names, in every pass: on the flat tsc-ts bundle, 9.2 million receiver tests per build. It now indexes each receiver list once by the method, generic method, accessor and field names along each receiver's base chain, and tests only the receivers listed under a name, plus the ones whose emitter, stream or EventTarget root answers names of its own, in list order. Names that the test decides on every receiver (compiler names, constructor, stack) and emit-override specializations still test every receiver. SCRIPTC_CHECK_DISPATCH_CANDIDATES=1 checks each selection against a test of every receiver. --- .../lowering/class-dynamic-dispatch.ts | 149 ++++++++++++++++-- 1 file changed, 132 insertions(+), 17 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index fefe85d0c4..79edae4334 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -1,4 +1,5 @@ import { primitiveConstructorOr } from "./lower-exprs.js"; +import { InternalCompilerError } from "../../errors.js"; import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js"; import { constructedValue } from "./constructor-returns.js"; import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js"; @@ -467,6 +468,77 @@ function sortAdded(lowerer: Lowerer, set: Set, from: number): void { for (const name of added) set.add(name); } +/** The receivers of one property-receiver list (propertyReceivers) by the + * names that can make them answer a member, as positions in the list. A + * receiver whose root answers names of its own (an emitter, a stream, an + * EventTarget or a Date) is in `rooted` instead. */ +interface ReceiverIndex { + plans: readonly PropertyReceiver[]; + /** By a method or generic method its base chain declares. */ + methods: Map; + /** By a method, generic method or accessor its base chain declares (an + * accessor under its property name), or a field it has. */ + members: Map; + rooted: number[]; +} + +function receiverIndex(lowerer: Lowerer, plans: readonly PropertyReceiver[]): ReceiverIndex { + const methods = new Map(); + const members = new Map(); + const rooted: number[] = []; + const add = (table: Map, name: string, position: number): void => { + const positions = table.get(name); + if (positions === undefined) table.set(name, [position]); + else if (positions[positions.length - 1] !== position) positions.push(position); + }; + plans.forEach(({ info }, position) => { + if ( + emitterRooted(lowerer, info) || + info.builtinStream !== undefined || + eventTargetApiMembers(info) !== null || + dateRootOf(info) !== null + ) { + rooted.push(position); + return; + } + for (let c: ClassInfo | null = info; c; c = c.base) { + for (const name of c.methods.keys()) { + add(methods, name, position); + add( + members, + name.startsWith("get:") || name.startsWith("set:") ? name.slice(4) : name, + position, + ); + } + for (const name of c.genericMethods?.keys() ?? []) { + add(methods, name, position); + add(members, name, position); + } + } + for (const name of info.fields.keys()) add(members, name, position); + }); + for (const table of [methods, members]) + for (const positions of table.values()) positions.sort((a, b) => a - b); + return { plans, methods, members, rooted }; +} + +/** The ascending union of two ascending position lists. */ +function mergePositions(left: readonly number[], right: readonly number[]): number[] { + const out: number[] = []; + let i = 0; + let j = 0; + while (i < left.length || j < right.length) { + const next = + j >= right.length || (i < left.length && left[i]! <= right[j]!) ? left[i++]! : right[j++]!; + if (out[out.length - 1] !== next) out.push(next); + } + return out; +} + +/** SCRIPTC_CHECK_DISPATCH_CANDIDATES=1 checks every indexed receiver selection + * against a test of every receiver. */ +const CHECK_CANDIDATES = process.env["SCRIPTC_CHECK_DISPATCH_CANDIDATES"] === "1"; + export function classPropertiesHelper(lowerer: Lowerer, loc: SrcLoc, readOnly = false): IrFunction { const name = readOnly ? READ_PROPERTIES : "%dyn.class.properties"; lowerer.shareHelper(name); @@ -1342,10 +1414,31 @@ export class ClassDynamicDispatch { } refreshDescriptorGuards(lowerer); const byMethod = new Map(); + // A query asks every receiver about one name, and the program's dispatch + // sites ask about thousands: index the receivers by name once per list. + let index: ReceiverIndex | null = null; + const indexOf = (plans: readonly PropertyReceiver[]): ReceiverIndex => + index?.plans === plans ? index : (index = receiverIndex(lowerer, plans)); + /** The receivers of `plans` that can answer `name`, in list order: those + * `table` lists under it and the rooted ones. Emit-override + * specializations (`emit:`) join a class's methods while bodies + * lower, so they keep every receiver. */ + const receiversFor = ( + plans: readonly PropertyReceiver[], + name: string, + table: "methods" | "members", + ): readonly PropertyReceiver[] => { + if (name.startsWith("emit:")) return plans; + const found = indexOf(plans); + return mergePositions(found[table].get(name) ?? [], found.rooted).map( + (position) => plans[position]!, + ); + }; const candidates = (method: string): ClassInfo[] => { const found = byMethod.get(method); if (found) return found; - const matching = this.propertyReceivers(lowerer, true).flatMap(({ info }) => { + const plans = this.propertyReceivers(lowerer, true); + const test = ({ info }: PropertyReceiver): ClassInfo[] => { if (!info || info.fields.has(method) || info.builtinError) return []; if (emitterRooted(lowerer, info) && EMITTER_METHODS.has(method)) return [info]; if ( @@ -1364,7 +1457,13 @@ export class ClassDynamicDispatch { return findMethodOn(lowerer, info, method) || findGenericMethodOn(lowerer, info, method) ? [info] : []; - }); + }; + const matching = receiversFor(plans, method, "methods").flatMap(test); + if (CHECK_CANDIDATES) { + const every = plans.flatMap(test); + if (every.length !== matching.length || every.some((info, i) => info !== matching[i])) + throw new InternalCompilerError(`indexed candidates for ${method} differ`); + } byMethod.set(method, matching); return matching; }; @@ -2898,24 +2997,40 @@ export class ClassDynamicDispatch { }); changed = true; } - for (const plan of this.propertyReceivers(lowerer, true)) { + // Only the receivers that declare or hold the name, and the rooted + // ones, can pass the test, except for names it decides on every + // receiver. Fields added while bodies lower (the property bag and the + // attribute words) have compiler names, which take every receiver. + const plans = this.propertyReceivers(lowerer, true); + const anyReceiver = + !isClassOwnEnumerableFieldName(dispatch.name) || + dispatch.name === "constructor" || + dispatch.name === "stack"; + const skips = (plan: PropertyReceiver): boolean => + !isClassOwnEnumerableFieldName(dispatch.name) || + (!(plan.info.builtinStream !== undefined && streamProperty(dispatch.name)) && + !eventTargetInheritedMember(plan.info, dispatch.name) && + !dateInheritedMember(lowerer, plan.info, dispatch.name) && + !findMethodOn(lowerer, plan.info, dispatch.name) && + !findGenericMethodOn(lowerer, plan.info, dispatch.name) && + dispatch.name !== "constructor" && + !plan.info.fields.has(dispatch.name) && + !(dispatch.name === "stack" && lowerer.isSubclassOf(plan.info.def.name, "%Error")) && + !findMethodOn(lowerer, plan.info, `get:${dispatch.name}`) && + !findMethodOn(lowerer, plan.info, `set:${dispatch.name}`)); + const selected = anyReceiver ? plans : receiversFor(plans, dispatch.name, "members"); + if ( + CHECK_CANDIDATES && + plans.some( + (plan) => !dispatch.classes.has(plan.key) && !skips(plan) && !selected.includes(plan), + ) + ) + throw new InternalCompilerError(`indexed receivers for ${dispatch.name} differ`); + for (const plan of selected) { const { info } = plan; // Classes answered in an earlier pass skip the member lookups. if (dispatch.classes.has(plan.key)) continue; - if ( - !isClassOwnEnumerableFieldName(dispatch.name) || - (!(info.builtinStream !== undefined && streamProperty(dispatch.name)) && - !eventTargetInheritedMember(info, dispatch.name) && - !dateInheritedMember(lowerer, info, dispatch.name) && - !findMethodOn(lowerer, info, dispatch.name) && - !findGenericMethodOn(lowerer, info, dispatch.name) && - dispatch.name !== "constructor" && - !info.fields.has(dispatch.name) && - !(dispatch.name === "stack" && lowerer.isSubclassOf(info.def.name, "%Error")) && - !findMethodOn(lowerer, info, `get:${dispatch.name}`) && - !findMethodOn(lowerer, info, `set:${dispatch.name}`)) - ) - continue; + if (skips(plan)) continue; dispatch.classes.add(plan.key); const loc = dispatch.fn.loc; const { receiver, condition } = this.propertyReceiver(plan, loc); From 930ee4e63a5f79d3d9414f596cae814e8e6b6c06 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 07:20:38 -0500 Subject: [PATCH 132/395] Answer binary requests in the literal-values test transport Object replies are read through binary() since the checker objects are resolved from their ids, so the test transport answers both forms. --- .../src/frontend/literal-values.test.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/frontend/literal-values.test.ts b/packages/compiler/src/frontend/literal-values.test.ts index 2a85f82bf9..1d792b1d4c 100644 --- a/packages/compiler/src/frontend/literal-values.test.ts +++ b/packages/compiler/src/frontend/literal-values.test.ts @@ -8,16 +8,15 @@ import { function harness() { const members = new Map(); - const snapshot = new SemanticSnapshot(1, { - text(method, payload) { - const query = JSON.parse(payload) as { objectId: number }; - if (method !== "getTypesOfType" || !members.has(query.objectId)) - throw new Error(`unexpected request: ${method}`); - return JSON.stringify(members.get(query.objectId)); - }, - binary(method) { + const reply = (method: string, payload: string) => { + const query = JSON.parse(payload) as { objectId: number }; + if (method !== "getTypesOfType" || !members.has(query.objectId)) throw new Error(`unexpected request: ${method}`); - }, + return JSON.stringify(members.get(query.objectId)); + }; + const snapshot = new SemanticSnapshot(1, { + text: reply, + binary: (method, payload) => new TextEncoder().encode(reply(method, payload)), }); const project = snapshot.addProject("literal-values", () => undefined); let id = 0; From 51b9bdca11d21e7ee2b670df375c636c2020b286 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 21 Oct 2026 21:39:25 -0700 Subject: [PATCH 133/395] 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 0000000000..6599e35f93 --- /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 0000000000..2c036408ea --- /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 39b7207c03..53971c7b02 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 b02a0a843c..b1867be326 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 86a41d3a68..f9cf1deb67 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 134/395] 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 2c036408ea..a8af7a19ed 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 aa90fc7c74..0e0ddf9757 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 135/395] 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 0000000000..d0b5c8924d --- /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 0000000000..f4bfb180e2 --- /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 a8af7a19ed..0d18711706 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 184fdb99e9..6e10e9c626 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 53971c7b02..3c04924c0b 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 19123dae3d..ea844ab1ea 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 f9cf1deb67..3af7ebf594 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 fcdbe86947..29385db370 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 bb14d063e2..8d1c7b058c 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 136/395] 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 b1867be326..ac1a778cff 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 ff01acbb8c..a8620c1cef 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 0000000000..8f26237179 --- /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 0000000000..fa7ceba3eb --- /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 0000000000..c9e0dc52ba --- /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 0000000000..8295d92523 --- /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 0000000000..8bc993da5d --- /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 0000000000..4366dee0d8 --- /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 0000000000..58f133abe9 --- /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 0000000000..eaa7bed6cb --- /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 0000000000..0e8a9ccb86 --- /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 0000000000..fbb6ce0fc9 --- /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 0000000000..db44a78519 --- /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 0000000000..265d7f8581 --- /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 0000000000..3804daa8df --- /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 0000000000..d17ce11678 --- /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 0000000000..5ffd9800b9 --- /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 137/395] 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 0000000000..545c4d701c --- /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 0000000000..e60660c09a --- /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 138/395] 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 6599e35f93..494a9c69f7 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 3c04924c0b..9d6f477f64 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 139/395] 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 494a9c69f7..2d34c5c532 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 d0b5c8924d..2f8ca05804 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 9d6f477f64..b650ca5672 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 ac1a778cff..206c42fdce 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 3af7ebf594..a0c2e4cb96 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 140/395] 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 2f8ca05804..aa752d703c 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 141/395] 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 aa752d703c..b407132f47 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 3800a7d31158b3ff72559b53d779b281da1917ac Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 07:37:58 -0500 Subject: [PATCH 142/395] Version dense array loops bounded by an invariant limit A counted loop whose limit is an invariant value other than the array's length, such as `j < n` after `const n = a.length`, gets the same fast copy as a loop to `a.length`: one test before the loop bounds the limit by 2^31 - 1 and by each indexed array's hole-free prefix, for every array the loop provably never resizes, and the fast copy reads those slots directly. The length proof accepts number and boolean writes to class fields, which run no user code, and borrowed element reads in a fast copy load the slot without the prefix test or the runtime call. The fast copy's loop body may hold up to 160 IR nodes. --- .../backend/llvm/dense-array-loops.test.ts | 85 +++++++++++---- .../src/backend/llvm/dense-array-loops.ts | 85 +++++++++++---- .../src/backend/llvm/local-array-reads.ts | 18 ++++ .../src/ir/array-length-stability.test.ts | 26 +++++ .../compiler/src/ir/array-length-stability.ts | 9 ++ ...-array-dense-loop-invariant-limits.ts.json | 6 ++ .../7320-array-dense-loop-invariant-limits.ts | 101 ++++++++++++++++++ ...-array-dense-loop-invariant-limits.ts.json | 1 + 8 files changed, 291 insertions(+), 40 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7320-array-dense-loop-invariant-limits.ts.json create mode 100644 tests/corpus/7320-array-dense-loop-invariant-limits.ts create mode 100644 tests/harness/corpus-costs/7320-array-dense-loop-invariant-limits.ts.json diff --git a/packages/compiler/src/backend/llvm/dense-array-loops.test.ts b/packages/compiler/src/backend/llvm/dense-array-loops.test.ts index 405ad67e7e..32c9cccab0 100644 --- a/packages/compiler/src/backend/llvm/dense-array-loops.test.ts +++ b/packages/compiler/src/backend/llvm/dense-array-loops.test.ts @@ -1,5 +1,6 @@ import { expect, test } from "vitest"; import { BOOL, F64, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js"; +import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; import { countedDenseShape, matchDenseArrayLoop, @@ -111,16 +112,23 @@ test("loops that may resize, rebind or move storage keep the ordinary copy only" expect([...rebound!.arrays.keys()]).toEqual(["a"]); }); +const counted = ( + fields: Pick, +): IntegerCountedForLoop => ({ + localId: "i", + guarded: false, + limitGuarded: false, + startGuarded: false, + guardLimit: 0, + ...fields, +}); + test("counted shapes need a non-negative counter strictly below the length", () => { const ascending = (limit: IrExpr, inclusive: boolean, min = 0) => - countedDenseShape(loop([]), { - localId: "i", - start: num(min), - limit, - step: 1, - inclusive, - range: { min, max: 100 }, - }); + countedDenseShape( + loop([]), + counted({ start: num(min), limit, step: 1, inclusive, range: { min, max: 100 } }), + ); const minus = (offset: number): IrExpr => ({ kind: "bin", op: "-", @@ -129,19 +137,58 @@ test("counted shapes need a non-negative counter strictly below the length", () type: F64, loc, }); - expect(ascending(method("a", "length"), false)?.length.localId).toBe("a"); - expect(ascending(minus(1), true)?.length.localId).toBe("a"); + expect(ascending(method("a", "length"), false)?.length?.localId).toBe("a"); + expect(ascending(minus(1), true)?.length?.localId).toBe("a"); expect(ascending(method("a", "length"), true)).toBeNull(); expect(ascending(minus(1), false, -1)).toBeNull(); + // Another invariant limit bounds the counter itself; the entry test + // compares it with every indexed array's hole-free prefix. + const limited = ascending(ref("n"), false); + expect(limited?.length).toBeNull(); + expect(limited?.limit?.limit).toEqual(ref("n")); + expect(ascending(ref("n"), false, -1)).toBeNull(); const descending = (offset: number) => - countedDenseShape(loop([]), { - localId: "i", - start: minus(offset), - limit: num(0), - step: -1, - inclusive: true, - range: { min: 0, max: 100 }, - }); - expect(descending(1)?.length.localId).toBe("a"); + countedDenseShape( + loop([]), + counted({ + start: minus(offset), + limit: num(0), + step: -1, + inclusive: true, + range: { min: 0, max: 100 }, + }), + ); + expect(descending(1)?.length?.localId).toBe("a"); expect(descending(0)).toBeNull(); }); + +test("a loop to another invariant limit covers each array whose length it keeps", () => { + const limited: DenseLoopShape = { + counter: "i", + length: null, + limit: counted({ + start: num(0), + limit: ref("n"), + step: 1, + inclusive: false, + range: { min: 0, max: 100 }, + }), + range: { min: 0, max: 100 }, + }; + const matchLimited = (body: IrStmt[], unaliased = new Set()) => + matchDenseArrayLoop(loop(body), limited, locals, unaliased, new Set(["a", "b"])); + const copy = write("b", read("a")); + const dense = matchLimited([copy]); + expect([...dense!.arrays.keys()].sort()).toEqual(["a", "b"]); + expect(dense!.length).toBeNull(); + expect(dense!.limit?.limit).toEqual(ref("n")); + // An array the loop may resize is not covered; neither is one it may + // share storage with. + const resizing = [ + { kind: "exprStmt" as const, expr: read("a"), loc }, + { kind: "exprStmt" as const, expr: method("b", "getNumber", [ref("i")]), loc }, + { kind: "exprStmt" as const, expr: method("b", "push", [num(1)]), loc }, + ]; + expect([...matchLimited(resizing, new Set(["b"]))!.arrays.keys()]).toEqual(["a"]); + expect(matchLimited(resizing)).toBeNull(); +}); diff --git a/packages/compiler/src/backend/llvm/dense-array-loops.ts b/packages/compiler/src/backend/llvm/dense-array-loops.ts index 7088253441..184ff13532 100644 --- a/packages/compiler/src/backend/llvm/dense-array-loops.ts +++ b/packages/compiler/src/backend/llvm/dense-array-loops.ts @@ -15,22 +15,33 @@ * test bounds `a.length` by 2^31 - 1, so the fast copy's counter is an int32. * Arrays longer than that are sparse in practice (a dense one would need * 16 GB of slots) and take the ordinary copy, as does every array with a - * hole, present undefined or sparse storage below `a.length`. */ + * hole, present undefined or sparse storage below `a.length`. + * + * A counted loop with another invariant limit, such as `j < n` after + * `const n = a.length` or `j < 5`, is covered the same way: the test bounds + * the loop's exclusive limit by 2^31 - 1 and by each indexed array's + * `filled`, and the loop must provably never change any of their lengths. */ import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js"; import type { IntegerRange } from "../../ir/integer-ranges.js"; import { preservesArrayLength } from "../../ir/array-length-stability.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; +import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { loadArrCount, SCR_ARR_FIELD } from "./common.js"; +import { emitCountedLoopLimit } from "./counted-loops.js"; export const INT32_MAX = 2147483647; export interface DenseArrayLoop { /** The loop's counter binding. */ counter: string; - /** The array whose length bounds the counter. */ - length: IrExpr & { kind: "varRef" }; + /** The array whose length bounds the counter, or null for a counted loop + * bounded by `limit`. */ + length: (IrExpr & { kind: "varRef" }) | null; + /** The ascending counted loop whose invariant limit bounds the counter, + * when `length` is null. */ + limit: IntegerCountedForLoop | null; /** Arrays whose accesses at the counter are proven to be inside the * hole-free prefix in the fast copy, with a read of each binding. */ arrays: ReadonlyMap; @@ -39,12 +50,15 @@ export interface DenseArrayLoop { } /** The fast copy duplicates the loop; keep the duplicated code small. */ -const MAX_LOOP_NODES = 120; +const MAX_LOOP_NODES = 160; /** How a matched integer loop bounds its counter below `length.length`. */ export interface DenseLoopShape { counter: string; - length: IrExpr & { kind: "varRef" }; + /** The array whose length bounds the counter, or null for `limit`. */ + length: (IrExpr & { kind: "varRef" }) | null; + /** An ascending counted loop bounded by another invariant limit. */ + limit?: IntegerCountedForLoop; /** The counter's proven range on every pass of the ordinary loop; the * fast copy keeps its minimum, which must be at least 0. */ range: IntegerRange; @@ -73,22 +87,18 @@ function lengthOffset(e: IrExpr): { receiver: IrExpr & { kind: "varRef" }; offse /** The array bounding a counted loop's counter below its length: an * ascending loop compared below `a.length - c`, or a descending one starting - * at `a.length - c` (c >= 1). */ + * at `a.length - c` (c >= 1). Another ascending loop is bounded by its own + * invariant limit. */ export function countedDenseShape( loop: IrStmt & { kind: "for" }, - counted: { - localId: string; - start: IrExpr; - limit: IrExpr; - step: number; - inclusive: boolean; - range: IntegerRange; - }, + counted: IntegerCountedForLoop, ): DenseLoopShape | null { if (counted.range.min < 0) return null; if (counted.step > 0) { const bound = lengthOffset(counted.limit); - if (!bound || bound.offset < (counted.inclusive ? 1 : 0)) return null; + if (!bound) + return { counter: counted.localId, length: null, limit: counted, range: counted.range }; + if (bound.offset < (counted.inclusive ? 1 : 0)) return null; return { counter: counted.localId, length: bound.receiver, range: counted.range }; } // The start is evaluated once, before the loop; the counter only falls. @@ -143,9 +153,11 @@ export function matchDenseArrayLoop( return everyStmtChild(s, expr, stmt); }; if (!loop.body.every(stmt) || !expr(loop.cond) || nested) return null; + const nodesOfLoop = [loop.cond, ...loop.body]; if ( + length !== null && !preservesArrayLength( - [loop.cond, ...loop.body], + nodesOfLoop, { receiver: length.localId, inRange: counter }, locals, unaliased, @@ -161,7 +173,14 @@ export function matchDenseArrayLoop( local.boxed || local.tdz || disturbed.has(id) || - (id !== length.localId && !initialized.has(id)) + (id !== length?.localId && !initialized.has(id)) + ) + continue; + // Without a bounding array, the entry test reads each array's `filled` + // once: the loop must not change any of their lengths. + if ( + length === null && + !preservesArrayLength(nodesOfLoop, { receiver: id, inRange: counter }, locals, unaliased) ) continue; arrays.set(id, ref); @@ -170,13 +189,15 @@ export function matchDenseArrayLoop( return { counter, length, + limit: length === null ? (shape.limit ?? null) : null, arrays, range: { min: shape.range.min, max: Math.min(shape.range.max, INT32_MAX - 1) }, }; } -/** The fast copy's entry test: the bounding length is at most 2^31 - 1 and - * at most every indexed array's hole-free prefix bound. */ +/** The fast copy's entry test: the bounding length (or the counted loop's + * exclusive limit) is at most 2^31 - 1 and at most every indexed array's + * hole-free prefix bound. */ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoop): string { const B = host.B; const size = host.sizeType; @@ -186,7 +207,29 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo host.markMemoryPointer(ptr, "array:header"); return ptr; }; - const receiver = host.emitStableReceiver(loop.length, []); + if (loop.length === null) { + // A limit at or below the start runs no pass, and passes the test. + const limit = emitCountedLoopLimit(host, loop.limit!); + const small = B.tmp(); + B.line(`${small} = icmp sle i64 ${limit}, ${INT32_MAX}`); + let valid = small; + for (const ref of loop.arrays.values()) { + const array = host.emitStableReceiver(ref, []); + const filledPtr = header(array.name, SCR_ARR_FIELD.filled), + filled = B.tmp(), + wide = B.tmp(), + covered = B.tmp(), + both = B.tmp(); + B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`); + B.line(`${wide} = zext i32 ${filled} to i64`); + B.line(`${covered} = icmp sle i64 ${limit}, ${wide}`); + B.line(`${both} = and i1 ${valid}, ${covered}`); + valid = both; + } + return valid; + } + const length = loop.length; + const receiver = host.emitStableReceiver(length, []); const lenPtr = header(receiver.name, SCR_ARR_FIELD.len), len = B.tmp(), small = B.tmp(); @@ -194,7 +237,7 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo B.line(`${small} = icmp ule ${size} ${len}, ${INT32_MAX}`); let valid = small; for (const [id, ref] of loop.arrays) { - const array = id === loop.length.localId ? receiver : host.emitStableReceiver(ref, []); + const array = id === length.localId ? receiver : host.emitStableReceiver(ref, []); const filledPtr = header(array.name, SCR_ARR_FIELD.filled), filled = B.tmp(), covered = 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 45d046e1f4..cdd00c4e7c 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -586,6 +586,24 @@ export function emitBorrowedArrayRead( const B = host.B; const array = host.emitStableReceiver(read.arr, [read.index]); const loopIndex = host.emitIntegerLoopIndex(read.index); + if (loopIndex !== null && host.provenDenseIndex(read.arr, read.index)) { + // In a dense array loop's fast copy, the entry test proved the counter + // below the array's hole-free prefix: the slot holds a present value. + const dataPtr = B.tmp(), + data = B.tmp(), + valuePtr = B.tmp(), + value = B.tmp(); + B.line( + `${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.data}`, + ); + host.markMemoryPointer(dataPtr, "array:header"); + B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`); + 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)}`); + B.line(`store ptr ${value}, ptr ${slot}`); + 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. diff --git a/packages/compiler/src/ir/array-length-stability.test.ts b/packages/compiler/src/ir/array-length-stability.test.ts index a0d0e49a77..f9b3cc9c2c 100644 --- a/packages/compiler/src/ir/array-length-stability.test.ts +++ b/packages/compiler/src/ir/array-length-stability.test.ts @@ -167,3 +167,29 @@ test("fresh local arrays read only as receivers have no alias", () => { const rebound = [...body, { kind: "assign", localId: "out", value: arr("a"), loc } as IrStmt]; expect(findUnaliasedLocalArrays(fn(rebound)).has("out")).toBe(false); }); + +test("number and boolean writes to class fields run no user code", () => { + const obj: IrExpr = { + kind: "varRef", + localId: "o", + type: { kind: "object", className: "P" }, + loc, + }; + const scope = new Map(locals).set("o", { + id: "o", + name: "o", + type: { kind: "object", className: "P" }, + mutable: false, + }); + const fieldSet = (value: IrExpr): IrStmt => ({ + kind: "fieldSet", + obj, + className: "P", + field: "x", + value, + loc, + }); + expect(preserves([fieldSet(read("a", ref("i")))], "i", new Set(), scope)).toBe(true); + // A reference stored into a field releases the old one. + expect(preserves([fieldSet(arr("b"))], "i", new Set(), scope)).toBe(false); +}); diff --git a/packages/compiler/src/ir/array-length-stability.ts b/packages/compiler/src/ir/array-length-stability.ts index 3f9c3b9caa..ed57972b6b 100644 --- a/packages/compiler/src/ir/array-length-stability.ts +++ b/packages/compiler/src/ir/array-length-stability.ts @@ -264,6 +264,15 @@ export function preservesArrayLength( ); case "return": return s.value === null || expr(s.value); + case "fieldSet": + // A number or boolean stored into a class field: accessors are + // calls, not field writes, and the old value holds no reference to + // release, so the write runs no user code. + return ( + (s.value.type.kind === "f64" || s.value.type.kind === "bool") && + expr(s.obj) && + expr(s.value) + ); default: if (!PLAIN_STATEMENTS.has(s.kind)) return false; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7320-array-dense-loop-invariant-limits.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7320-array-dense-loop-invariant-limits.ts.json new file mode 100644 index 0000000000..785bf9d822 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7320-array-dense-loop-invariant-limits.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7320-array-dense-loop-invariant-limits.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7320-array-dense-loop-invariant-limits.ts b/tests/corpus/7320-array-dense-loop-invariant-limits.ts new file mode 100644 index 0000000000..878245e609 --- /dev/null +++ b/tests/corpus/7320-array-dense-loop-invariant-limits.ts @@ -0,0 +1,101 @@ +// Index loops bounded by an invariant limit other than the array's own length +// (a local copy of a length, a literal, an inclusive bound), over arrays the +// loop never resizes: class instances whose number and boolean fields the loop +// writes, and number arrays that are dense, short, holey or partially filled. + +class Body { + x: number; + v = 0; + hit = false; + next: Body | null = null; + + constructor(x: number) { + this.x = x; + } +} + +function pairs(bodies: Body[]): string { + const n = bodies.length; + for (let i = 0; i < n; i++) { + const bi = bodies[i]!; + let vi = bi.v; + for (let j = i + 1; j < n; j++) { + const bj = bodies[j]!; + const d = bi.x - bj.x; + const f = 1 / (d * d + 1); + vi -= d * f; + bj.v += d * f; + bj.hit = f > 0.1; + } + bi.v = vi; + } + return bodies.map((b) => `${b.v.toFixed(6)}${b.hit ? "*" : ""}`).join(" "); +} + +function firstThree(bodies: Body[]): number { + let s = 0; + for (let j = 0; j < 3; j++) s += bodies[j]!.x; + return s; +} + +function inclusive(bodies: Body[]): number { + const last = bodies.length - 1; + let s = 0; + for (let j = 1; j <= last; j++) s += bodies[j]!.x * j; + return s; +} + +function linked(bodies: Body[]): string { + const n = bodies.length; + // A reference field write: the loop keeps the ordinary copy. + for (let j = 0; j < n - 1; j++) bodies[j]!.next = bodies[j + 1]!; + let count = 0; + for (let b: Body | null = bodies[0]!; b !== null; b = b.next) count++; + return `${count}`; +} + +function sumTo(a: number[], limit: number): string { + const n = limit; + let s = 0; + let missing = 0; + for (let i = 0; i < n; i++) { + const v = a[i]; + if (v === undefined) missing++; + else s += v; + } + return `${s} ${missing}`; +} + +function scaleTo(a: number[], out: number[]): string { + const n = a.length; + for (let i = 0; i < n; i++) out[i] = a[i] * 2; + return `${out.length}:${out.join(",")}`; +} + +function window(a: number[]): number { + let s = 0; + for (let i = 2; i < 6; i++) s += a[i] * i; + return s; +} + +const bodies = [new Body(0), new Body(1.5), new Body(-2), new Body(4), new Body(0.25)]; +console.log(pairs(bodies)); +console.log(pairs(bodies)); +console.log(firstThree(bodies), inclusive(bodies), inclusive([new Body(7)])); +console.log(linked([new Body(1), new Body(2), new Body(3)])); + +const dense = [1, 2, 3, 4, 5, 6, 7, 8]; +console.log(sumTo(dense, 8), sumTo(dense, 5), sumTo(dense, 0), sumTo(dense, -3)); +// Limits past the length or the filled prefix take the checked loop. +console.log(sumTo(dense, 10), sumTo([], 2)); +const holey = [1]; +holey[3] = 4; +console.log(sumTo(holey, 4), sumTo(holey, 1)); +const shortened = [1, 2, 3, 4, 5, 6]; +shortened.length = 3; +shortened.length = 6; +console.log(sumTo(shortened, 6), sumTo(shortened, 3)); +console.log(scaleTo(dense, new Array(8).fill(0))); +console.log(scaleTo(dense, [9, 9])); +console.log(scaleTo([1, 2, 3], [])); +console.log(window(dense), window([1, 2, 3, 4, 5, 6])); diff --git a/tests/harness/corpus-costs/7320-array-dense-loop-invariant-limits.ts.json b/tests/harness/corpus-costs/7320-array-dense-loop-invariant-limits.ts.json new file mode 100644 index 0000000000..2ea93101c5 --- /dev/null +++ b/tests/harness/corpus-costs/7320-array-dense-loop-invariant-limits.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 31600 } From c297971f130d183cb0f39fa1a8991d0728e96340 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 07:41:13 -0500 Subject: [PATCH 143/395] Report JSX-lowered files to the checker server as changed The program the lowering compiles serves each file that has JSX at its own path, with the JSX rewritten to calls (host.addVirtualFile). The host reported such a path to the checker server as created whenever it was not virtual yet. The files exist on disk and the server had already read them for the program that checks them as written, so tsgo kept their original text, and lowering met the JSX syntax: SC1090 "syntax 'JsxFragment' is not supported yet" in 6822, 6824 and 6825 under SCRIPTC_CHECKER=tsgo, while tsc-ts reread the files. A virtual file that replaces a file on disk is now reported as changed, and both checkers give the same LLVM IR and diagnostics for the JSX corpus programs. --- packages/compiler/src/frontend/ts7/program-host.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index c87da0bfe5..639086e945 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -192,7 +192,10 @@ export class Ts7Host { const key = tsgoPath(path); if (this.virtualFiles.get(key) === content) return; if (!this.virtualChanges.has(key)) - this.virtualChanges.set(key, this.virtualFiles.has(key) ? "changed" : "created"); + this.virtualChanges.set( + key, + this.virtualFiles.has(key) || trackedFileExists(path) ? "changed" : "created", + ); this.virtualFiles.set(key, content); } From 906f9911a148b4e1f54c0b4e195bafa92b629824 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 07:45:48 -0500 Subject: [PATCH 144/395] Keep a callback's captured scalars in registers in loops that call it A call through a binding that the constant-callback census resolves keeps a fallback through the closure's code pointer. In a loop that fallback is an unknown call that takes the closure, so the closure's code pointer, its captures and a captured `let` were reloaded, and the `let` stored back, on every iteration of `let s = 0; a.forEach((v) => { s = (s + v) | 0; })`. A for, while or do-while loop that calls such a binding now tests once, before the loop, that the binding's closure has the known code pointer. The copy where the test holds calls the function directly with no fallback, and the other copy is the ordinary loop. A closure's code pointer is set only when the closure is built and the census resolves only bindings the function never rebinds, so one test covers the loop. Only bindings declared before the loop qualify, and loops are versioned up to a size bound. Number and boolean box payloads get an alias category of their own, so a store to a captured `let` no longer aliases element loads and other memory in the loop. With the closure built in the caller, the test folds after inlining and the captured `let` stays in a register. --- .../src/backend/llvm/emit-function.ts | 2 + .../src/backend/llvm/emit-statements.ts | 117 ++++++++++++++ packages/compiler/src/backend/llvm/emitter.ts | 45 +++++- .../compiler/src/backend/llvm/expr-calls.ts | 27 ++-- .../test/callback-loop-versions.test.ts | 83 ++++++++++ .../7300-callback-captured-scalars.ts.json | 6 + .../corpus/7300-callback-captured-scalars.ts | 147 ++++++++++++++++++ .../7300-callback-captured-scalars.ts.json | 1 + 8 files changed, 406 insertions(+), 22 deletions(-) create mode 100644 packages/compiler/test/callback-loop-versions.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7300-callback-captured-scalars.ts.json create mode 100644 tests/corpus/7300-callback-captured-scalars.ts create mode 100644 tests/harness/corpus-costs/7300-callback-captured-scalars.ts.json diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 09e6551c30..9e8f7d7016 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -83,6 +83,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { } emitter.currentConstantCallbacks = emitter.debug === null ? (emitter.constantCallbacks.get(fn.name) ?? new Map()) : new Map(); + emitter.knownCallbacks.clear(); + emitter.callbackVersionsEnabled = emitter.debug === null && !fn.async && !fn.generator; // Preserve concrete source bindings for debugger inspection. Suspended // functions keep the established array lifetime across continuations. emitter.streamingSplitsEnabled = emitter.debug === null && !fn.async && !fn.generator; diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 7239db7d89..618464e88d 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -55,6 +55,7 @@ import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js"; import { mangleLocal } from "../mangle.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlValue } from "./expr-context.js"; +import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { widenInteger } from "./integer-values.js"; import { SCALAR_UNION_RESULT } from "./int32-abi.js"; import { emitLiteralSwitch } from "./switch-dispatch.js"; @@ -76,6 +77,9 @@ import { } from "./string-construction.js"; import { emitStringInputs } from "./string-lifetimes.js"; +/** A callback loop version copies the loop; keep the copies small. */ +const MAX_CALLBACK_VERSION_COST = 400; + export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const B = emitter.B; emitter.frames.push([]); @@ -795,6 +799,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { break; } case "while": { + const callbacks = callbackVersionBindings(emitter, s); + if (callbacks) { + emitCallbackVersionedLoop(emitter, s, callbacks); + break; + } const workerBudget = emitter.workerLoopBudget(s); const le = B.newLabel("loop.e"); emitter.emitLoopCopies(s, true, le, (exit) => { @@ -827,6 +836,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { break; } case "doWhile": { + const callbacks = callbackVersionBindings(emitter, s); + if (callbacks) { + emitCallbackVersionedLoop(emitter, s, callbacks); + break; + } const workerBudget = emitter.workerLoopBudget(s); // Body first (runs at least once); continue jumps to the CONDITION. const le = B.newLabel("loop.e"); @@ -855,6 +869,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { break; } case "for": { + const callbacks = callbackVersionBindings(emitter, s); + if (callbacks) { + emitCallbackVersionedLoop(emitter, s, callbacks); + break; + } // The init's scope wraps the whole loop (break/continue must NOT // release it — scopeDepth captured after the push, C parity). const integerArrayLoop = @@ -1885,6 +1904,104 @@ export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }): B.startBlock(end); } +/** The loop copies a callback loop version needs: the bindings the loop + * calls that the constant-callback census resolves to one function, each + * with that function's direct entry. Only small loops of optimized + * synchronous functions, and only bindings declared before the loop that + * an enclosing version has not already proved, qualify: the guard reads + * each binding before the loop runs. */ +function callbackVersionBindings( + emitter: LlEmitter, + s: IrStmt & { kind: "for" | "while" | "doWhile" }, +): { ref: IrExpr & { kind: "varRef" }; direct: string }[] | null { + if ( + !emitter.callbackVersionsEnabled || + emitter.currentConstantCallbacks.size === 0 || + emitter.callbackVersioning.has(s) + ) + return null; + const found = new Map(); + // Bindings the loop declares or writes (a callback declared in its body). + const bound = new Set(); + let cost = 0; + const expr = (e: IrExpr): boolean => { + if (++cost > MAX_CALLBACK_VERSION_COST) return false; + if (e.kind === "assignExpr") bound.add(e.localId); + if ( + e.kind === "callValue" && + e.callee.kind === "varRef" && + !emitter.knownCallbacks.has(e.callee.localId) && + !found.has(e.callee.localId) + ) { + const local = emitter.currentLocals.get(e.callee.localId); + const target = + local && local.tdz !== true ? emitter.knownCallbackTarget(e.callee) : undefined; + if (target) found.set(e.callee.localId, { ref: e.callee, direct: target.direct }); + } + return everyExprChild(e, expr, stmt); + }; + const stmt = (x: IrStmt): boolean => { + if (++cost > MAX_CALLBACK_VERSION_COST) return false; + if (x.kind === "varDecl" || x.kind === "assign" || x.kind === "forOf") bound.add(x.localId); + else if (x.kind === "tryCatch" && x.catchLocalId !== null) bound.add(x.catchLocalId); + return everyStmtChild(x, expr, stmt); + }; + if (!stmt(s)) return null; + const bindings = [...found.values()].filter(({ ref }) => !bound.has(ref.localId)); + return bindings.length > 0 ? bindings : null; +} + +/** A callback loop version. The guard tests, once before the loop, that + * each binding's closure holds its known function: a closure's code + * pointer is written only when the closure is built, and the census + * resolves only bindings the function never rebinds. The copy where the + * guard holds calls those functions directly, with no indirect fallback + * that could rewrite the closure or what it captures, so LLVM can keep a + * captured scalar the callback updates in a register. The other copy is the + * ordinary loop. */ +function emitCallbackVersionedLoop( + emitter: LlEmitter, + s: IrStmt & { kind: "for" | "while" | "doWhile" }, + bindings: { ref: IrExpr & { kind: "varRef" }; direct: string }[], +): void { + const B = emitter.B; + let ok = "true"; + for (const { ref, direct } of bindings) { + // A borrowed read: the guard only compares the code pointer. + const closure = emitter.emitReadReceiver(ref).name; + const fnp = B.tmp(), + fn = B.tmp(), + same = B.tmp(); + B.line(`${fnp} = getelementptr inbounds %ScrClosure, ptr ${closure}, i64 0, i32 1`); + B.line(`${fn} = load ptr, ptr ${fnp}`); + B.line(`${same} = icmp eq ptr ${fn}, ${direct}`); + if (ok === "true") ok = same; + else { + const both = B.tmp(); + B.line(`${both} = and i1 ${ok}, ${same}`); + ok = both; + } + } + const fast = B.newLabel("callback.version.fast"), + slow = B.newLabel("callback.version.slow"), + done = B.newLabel("callback.version.done"); + B.condBr(ok, fast, slow); + emitter.callbackVersioning.add(s); + try { + B.startBlock(slow); + emitter.emitStmt(s); + B.br(done); + B.startBlock(fast); + for (const { ref } of bindings) emitter.knownCallbacks.add(ref.localId); + emitter.emitStmt(s); + B.br(done); + } finally { + for (const { ref } of bindings) emitter.knownCallbacks.delete(ref.localId); + emitter.callbackVersioning.delete(s); + } + B.startBlock(done); +} + /** Emit a loop's condition, body and update through `emitCopy`, twice * 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 diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index ed17859126..9170db7d23 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -831,6 +831,12 @@ export class LlEmitter { readonly callLifetimes: CallLifetimes; readonly constantCallbacks: Map>; currentConstantCallbacks: ReadonlyMap = new Map(); + /** Callback bindings whose closures an enclosing callback loop version + * proved to hold their known function: calls take the direct entry with + * no fallback (emitCallbackVersionedLoop). */ + knownCallbacks = new Set(); + readonly callbackVersioning = new Set(); + callbackVersionsEnabled = false; readonly stackCallbacks: StackCallbacks; readonly receiverReaders: AmbientReceiverReaders; readonly stackCaptures: StackCaptures; @@ -3901,6 +3907,25 @@ export class LlEmitter { return b; } + /** The function a call through `callee` is known to reach (a literal + * closure, or a binding the constant-callback census resolves) and the + * entry it calls directly. Async and generator functions stay indirect. */ + knownCallbackTarget(callee: IrExpr): { known: IrFunction; direct: string } | undefined { + const knownName = + callee.kind === "closure" + ? callee.fnName + : callee.kind === "varRef" + ? this.currentConstantCallbacks.get(callee.localId) + : undefined; + const known = knownName ? this.fnByName.get(knownName) : undefined; + if (!known || known.async || known.generator) return undefined; + if (known.captures === undefined) { + this.fnValues.add(known.name); + return { known, direct: `@${mangleWrapper(known.name)}` }; + } + return { known, direct: `@${this.callTarget(known.name)}` }; + } + /** The payload slot of a capture box (ScrBox.slot, field 5). */ boxSlot(box: string): string { const slot = this.B.tmp(); @@ -3914,12 +3939,18 @@ export class LlEmitter { const r = B.tmp(); // Scalar payloads are read in place (scr_box_get_f64/bool are a // memcpy and a nonzero test of the same slot), so LLVM can keep a - // captured number in a register across a loop of calls. + // captured number in a register across a loop of calls. They have an + // alias category of their own: every other access to a box's payload + // is untagged or a runtime call. if (acc === "f64") { - B.line(`${r} = load double, ptr ${this.boxSlot(box)}`); + B.line( + `${r} = load double, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`, + ); } else if (acc === "bool") { const raw = B.tmp(); - B.line(`${raw} = load i64, ptr ${this.boxSlot(box)}`); + B.line( + `${raw} = load i64, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`, + ); B.line(`${r} = icmp ne i64 ${raw}, 0`); } else { this.declare(`declare ptr @scr_box_get_ref(ptr)`); @@ -3933,11 +3964,15 @@ export class LlEmitter { const B = this.B; const acc = boxAccess(t); if (acc === "f64") { - B.line(`store double ${value}, ptr ${this.boxSlot(box)}`); + B.line( + `store double ${value}, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`, + ); } else if (acc === "bool") { const raw = B.tmp(); B.line(`${raw} = zext i1 ${value} to i64`); - B.line(`store i64 ${raw}, ptr ${this.boxSlot(box)}`); + B.line( + `store i64 ${raw}, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`, + ); } else { this.declare(`declare void @scr_box_set_ref(ptr, ptr)`); B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${value})`); diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index f0aa3b276d..4bcf2a3d54 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -29,7 +29,6 @@ import { mangleClassStruct, mangleFnClosure, mangleFunction, - mangleWrapper, mangleVtStruct, mangleAsyncDirectSpawn, } from "../mangle.js"; @@ -839,21 +838,9 @@ export function emitCallExpr( host.declare("declare void @scr_dyn_this_push_dyn(ptr)"); B.line(`call void @scr_dyn_this_push_dyn(ptr ${receiver?.name ?? "null"})`); }; - const knownName = - e.callee.kind === "closure" - ? e.callee.fnName - : e.callee.kind === "varRef" - ? host.currentConstantCallbacks.get(e.callee.localId) - : undefined; - const candidate = knownName ? host.fnByName.get(knownName) : undefined; - const known = candidate && !candidate.async && !candidate.generator ? candidate : undefined; - let direct: string | undefined; - if (known) { - if (known.captures === undefined) { - host.fnValues.add(known.name); - direct = `@${mangleWrapper(known.name)}`; - } else direct = `@${host.callTarget(known.name)}`; - } + const target = host.knownCallbackTarget(e.callee); + const known = target?.known; + const direct = target?.direct; // A known closure body that borrows reference parameters is entered // directly with lent arguments, as a direct call to a borrowing body // is: an argument is read without a reference when neither a later @@ -939,7 +926,13 @@ export function emitCallExpr( let pending = host.indirectMayThrow; let pendingGuard: string | undefined; let t: string; - if (direct && e.callee.kind === "closure") { + // A literal closure, or a binding an enclosing callback loop version + // proved to hold its known function, enters it directly. + if ( + direct && + (e.callee.kind === "closure" || + (e.callee.kind === "varRef" && host.knownCallbacks.has(e.callee.localId))) + ) { t = invoke(lending ?? direct, !host.receiverReaders.ignores(known!)); if (quiet) pending = false; } else { diff --git a/packages/compiler/test/callback-loop-versions.test.ts b/packages/compiler/test/callback-loop-versions.test.ts new file mode 100644 index 0000000000..c26abe1c4c --- /dev/null +++ b/packages/compiler/test/callback-loop-versions.test.ts @@ -0,0 +1,83 @@ +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-callback-versions-")); + 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 body = (llvm: string, marker: string): string => { + const match = new RegExp(`^define internal [^\\n]*${marker}[^]*?^}`, "m").exec(llvm); + if (!match) throw new Error(`no function ${marker}`); + return match[0]; +}; + +test("loops that call a known callback test its closure once and call it directly", async () => { + const llvm = await emitLlvm(` + function each(values: number[], visit: (v: number) => void): void { + for (let i = 0; i < values.length; i++) visit(values[i]!); + } + let sum = 0; + each([1, 2, 3], (v) => { sum += v; }); + console.log(sum); + `); + const fn = body(llvm, "@sc_bf_each"); + const fast = /^callback\.version\.fast\d+:$[^]*?^callback\.version\.done\d+:$/m.exec(fn)![0]; + // The guard compares the closure's code pointer once, before the loop. + expect(fn.slice(0, fn.indexOf(fast))).toMatch(/icmp eq ptr %\S+, @/); + expect(fast).not.toContain("callback.indirect"); + expect(fast).not.toContain("scr_dyn_this_push_dyn"); + // The ordinary copy keeps the fallback through the closure's pointer. + expect(fn).toContain("callback.indirect"); +}); + +test("captured number payloads carry their own alias tag", async () => { + const llvm = await emitLlvm(` + function total(values: number[]): number { + let sum = 0; + values.forEach((v) => { sum += v; }); + return sum; + } + console.log(total([1, 2, 3])); + `); + // Every read and write through a box's payload slot (field 5) is tagged. + const slots = [ + ...llvm.matchAll(/(%\S+) = getelementptr inbounds %ScrBox, ptr %\S+, i32 0, i32 5/g), + ]; + expect(slots.length).toBeGreaterThan(0); + for (const [, slot] of slots) { + for (const access of llvm.matchAll( + new RegExp(`(load double, ptr|store double %\\S+, ptr) \\${slot}\\b[^\\n]*`, "g"), + )) + expect(access[0]).toMatch(/!tbaa !\d+$/); + } + expect(llvm).toMatch(/load double, ptr %\S+, !tbaa !\d+/); +}); + +test("a callback declared in the loop body is not read before the loop", async () => { + const llvm = await emitLlvm(` + function strides(): number { + let calls = 0; + for (let i = 0; i < 10; i++) { + const bump = (): void => { i += 2; }; + bump(); + calls++; + } + return calls; + } + console.log(strides()); + `); + expect(llvm).not.toContain("callback.version"); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7300-callback-captured-scalars.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7300-callback-captured-scalars.ts.json new file mode 100644 index 0000000000..dc11589348 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7300-callback-captured-scalars.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7300-callback-captured-scalars.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7300-callback-captured-scalars.ts b/tests/corpus/7300-callback-captured-scalars.ts new file mode 100644 index 0000000000..c8e8b28bdf --- /dev/null +++ b/tests/corpus/7300-callback-captured-scalars.ts @@ -0,0 +1,147 @@ +// Callbacks that update captured `let` numbers and booleans from loops: typed +// array and array methods, an authored higher-order function called with one +// function at several calls and with two functions at one, closures of one +// function over different bindings, a callback that throws part way, nested +// callbacks, callbacks declared in the loop body, and reads after the loop. + +function each(values: number[], visit: (v: number, i: number) => void): void { + for (let i = 0; i < values.length; i++) visit(values[i]!, i); +} + +function until(limit: number, step: (i: number) => boolean): number { + let i = 0; + while (i < limit) { + if (!step(i)) break; + i++; + } + return i; +} + +function twice(values: Int16Array, f: (v: number) => void): void { + let i = 0; + do { + for (let j = 0; j < values.length; j++) f(values[j]!); + i++; + } while (i < 2); +} + +const ints = new Int32Array([5, -3, 8, 0, 12, -7, 4]); +let total = 0; +let negatives = 0; +let sawZero = false; +ints.forEach((v) => { + total = (total + v) | 0; + if (v < 0) negatives++; + if (v === 0) sawZero = true; +}); +console.log("forEach", total, negatives, sawZero); + +let max = -Infinity; +let maxAt = -1; +const floats = new Float64Array([0.5, 9.25, -2, 9.25, 3]); +floats.forEach((v, i) => { + if (v > max) { + max = v; + maxAt = i; + } +}); +console.log("max", max, maxAt); + +let calls = 0; +const doubled = ints.map((v) => { + calls++; + return v * 2 + calls; +}); +console.log("map", doubled.join(","), calls); + +let seen = 0; +const big = [3, 14, 15, 92, 65].filter((v) => { + seen += v; + return v > 10; +}); +console.log("filter", big.join(","), seen); + +let sum = 0; +let count = 0; +const summing = (v: number): void => { + sum += v; + count++; +}; +each([1, 2, 3], summing); +each([10, 20], summing); +console.log("each shared", sum, count); + +let a = 0; +let b = 0; +each([1, 2], (v) => { + a += v; +}); +each([1, 2], (v, i) => { + b += v * (i + 1); +}); +console.log("each two", a, b); + +const counters: (() => number)[] = []; +for (let k = 0; k < 3; k++) { + let local = k * 100; + each([1, 1, 1], () => { + local++; + }); + counters.push(() => local); +} +console.log("closures", counters.map((c) => c()).join(",")); + +let steps = 0; +console.log("until", until(10, (i) => (steps += i, steps < 12)), steps); + +let shorts = 0; +twice(new Int16Array([7, -8, 9]), (v) => { + shorts = shorts * 2 + v; +}); +console.log("twice", shorts); + +let partial = 0; +try { + floats.forEach((v) => { + if (v < 0) throw new RangeError(`at ${partial}`); + partial += v; + }); +} catch (error) { + console.log("throw", (error as Error).message, partial); +} + +let outer = 0; +let inner = 0; +[1, 2, 3].forEach((x) => { + outer += x; + ints.forEach((y) => { + if (y > x) inner++; + }); +}); +console.log("nested", outer, inner); + +let flag = false; +let flips = 0; +new Uint8Array([1, 0, 1, 1]).forEach((v) => { + if (v === 1) flag = !flag; + if (flag) flips++; +}); +console.log("bool", flag, flips); + +// Callbacks declared in the loop body: each iteration binds its own. +let strides = 0; +for (let i = 0; i < 10; i++) { + const bump = (): void => { + i += 2; + strides++; + }; + bump(); +} +let countdown = 0; +let k = 3; +while (k > 0) { + const tick = (n: number): number => (n > 0 ? tick(n - 1) + 1 : 0); + countdown += tick(k); + k--; +} +console.log("declared in loop", strides, countdown); diff --git a/tests/harness/corpus-costs/7300-callback-captured-scalars.ts.json b/tests/harness/corpus-costs/7300-callback-captured-scalars.ts.json new file mode 100644 index 0000000000..18495cd4eb --- /dev/null +++ b/tests/harness/corpus-costs/7300-callback-captured-scalars.ts.json @@ -0,0 +1 @@ +{ "plain": 6000, "san": 7900 } From 164174f811a2f0f927fde6f76f5578c6ff538139 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 02:29:47 -0700 Subject: [PATCH 145/395] 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 2fc9a6cbda..7ce8ae074a 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 0000000000..83f8cc5d57 --- /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 0000000000..d4bc693f23 --- /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 0000000000..fae2adeb46 --- /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 146/395] 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 612c47ea04..dcadf3bf3b 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 0000000000..df57813fe2 --- /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 39b7207c03..3acfb256ac 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 0000000000..d77ee1a57c --- /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 40d5e462ad..0000000000 --- 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 0000000000..c2449b4696 --- /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 45452e212d..0000000000 --- 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 bc51802197..0000000000 --- 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 0000000000..188fa7fabe --- /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 147/395] 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 1ff35fbb51..9764216b7c 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 39b7207c03..7e104c2f18 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 0000000000..eacdfc1d42 --- /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 0000000000..c9ff338fa3 --- /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 0000000000..279e2fd66a --- /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 0000000000..bc9b033ff5 --- /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 0000000000..1b48d927bd --- /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 0000000000..f4eea833a0 --- /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 0000000000..272a4631c9 --- /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 0000000000..a2757b8a1e --- /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 0000000000..5ffd9800b9 --- /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 0000000000..d7f1a433a7 --- /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 0000000000..c06579627b --- /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 0000000000..8352ab7135 --- /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 0000000000..aead43de36 --- /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 0000000000..c72ce52881 --- /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 0000000000..26fdf628a0 --- /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 0000000000..f0a4be4c7a --- /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 0000000000..cb0ff5c3b5 --- /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 0000000000..e1640d1a4f --- /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 0000000000..91b2afb81b --- /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 0000000000..cb0ff5c3b5 --- /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 0000000000..cb0ff5c3b5 --- /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 0000000000..b9641f29b7 --- /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 0000000000..a9dab57383 --- /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 0000000000..3e97ad51a5 --- /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 0000000000..cb0ff5c3b5 --- /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 0000000000..de1e6213e0 --- /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 0000000000..8352ab7135 --- /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 0000000000..4e71355a38 --- /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 0000000000..52eacf72e4 --- /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 0000000000..f0a4be4c7a --- /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 0000000000..fad28a7110 --- /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 0000000000..bc9b033ff5 --- /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 0000000000..e4dc1d7b3f --- /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 0000000000..d07657a250 --- /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 0000000000..32df022ca3 --- /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 0000000000..eeaaeee1c4 --- /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 0000000000..a9dab57383 --- /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 0000000000..b2bea1ec4e --- /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 0000000000..7ddff3b7f3 --- /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 0000000000..301e09128e --- /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 0000000000..e02b87614b --- /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 0000000000..cda6a6168f --- /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 148/395] 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 b3faf649eb..75d584e4b6 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 0000000000..db4bc557f1 --- /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 0000000000..094b17f027 --- /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 0000000000..fae2adeb46 --- /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 149/395] 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 2a3f11be91..561dcc1249 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 af7270ea93..2e0f305a07 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 75e527aa5d..129ac73a36 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 2a77fa85c1..8619b4997b 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 53b0748b4c..f42663a134 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 5b5e283b32..c3b4ad906d 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 612c47ea04..3de1a4e6b1 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 8f119504cb..e5ccd1176b 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 678b375fee..d0edc8e283 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); - } - // 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)`, - ); + 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), + }, + }); } - 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 b5acaf9e62..6a80c551ed 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 213abfec6a..831fec83d2 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 b02a0a843c..d87ace9143 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 e3722bc324..126f80f936 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 bd2a32c375..aa275a27d9 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 0000000000..f6c5594a15 --- /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 0000000000..c1e5f1ae42 --- /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 0000000000..c1856ac5a3 --- /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 0000000000..bae130837f --- /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 0000000000..c4b9855ef1 --- /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 0000000000..ed0e7b6509 --- /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 0000000000..f050c4d1a3 --- /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 0000000000..9b69861b10 --- /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 0000000000..a40ef11802 --- /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 0000000000..72d564d3bb --- /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 0000000000..57e76b621a --- /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 0000000000..a78b23cb13 --- /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 057d24d553..0000000000 --- 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 0000000000..61c250174b --- /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 0000000000..4613519923 --- /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 0000000000..dca5cdcdee --- /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 0000000000..8717e892a0 --- /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 0000000000..8343e028b6 --- /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 0000000000..317c87db18 --- /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 0000000000..c2bc599589 --- /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 0000000000..580d6c0392 --- /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 0000000000..5d00952abe --- /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 0000000000..83c9583deb --- /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 0000000000..89c0f69e86 --- /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 0000000000..0fb93ff8d8 --- /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 e42f8a07a8..0ee60579d3 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 03043a32f0..0000000000 --- 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 f5f9f8f86c..a1f2999b73 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 743bb56572..2f6afe734a 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 707d1dfd95..0000000000 --- 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 56f8b81eec..eaced49791 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 0000000000..ee564e79fc --- /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 0000000000..fba6fd5308 --- /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 0000000000..c59bb483b9 --- /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 0000000000..3f0a047e7e --- /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 0000000000..6285a25c58 --- /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 0000000000..44843dc065 --- /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 0000000000..9d3f28ab11 --- /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 0000000000..56c46a74b3 --- /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 0000000000..147f70b9ae --- /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 0000000000..7efdd91bfc --- /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 0000000000..1fd3ee0530 --- /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 0000000000..7e342cfd07 --- /dev/null +++ b/tests/harness/corpus-costs/7034-optional-field-union-routes.ts.json @@ -0,0 +1 @@ +{ "plain": 2420, "san": 3640 } From f0c48e1b399cb7aa44ddfad988b678f9c6c3154d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:03:32 -0500 Subject: [PATCH 150/395] Bound compiled regex reads by the characters left in the subject The 16-character searches (the first-character search, the start filter and greedy class loops) and literal runs checked `i + n <= len` in 32-bit arithmetic. Subjects may be up to 2^31 - 2 characters, so near that length the sum wrapped negative, passed the check and read past the subject. Each check now compares the characters left (`len - i`, which cannot wrap) with the characters it reads, and advances the position only after it passes. --- .../src/backend/llvm/regex-matchers.ts | 28 +++--- .../test/regex-matcher-bounds.test.ts | 91 +++++++++++++++++++ 2 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 packages/compiler/test/regex-matcher-bounds.test.ts diff --git a/packages/compiler/src/backend/llvm/regex-matchers.ts b/packages/compiler/src/backend/llvm/regex-matchers.ts index 77f8a06ebc..d83d6bae54 100644 --- a/packages/compiler/src/backend/llvm/regex-matchers.ts +++ b/packages/compiler/src/backend/llvm/regex-matchers.ts @@ -882,16 +882,13 @@ class RxGen { this.block(head); const i = this.tmp(), i16 = this.tmp(), + rest = this.tmp(), fits = this.tmp(); this.line(`${i} = phi i32 [ ${a}, %seek ], [ ${i16}, %${next} ]`); - this.line(`${i16} = add i32 ${i}, 16`); - if (reach === 0) { - this.line(`${fits} = icmp sle i32 ${i16}, %len`); - } else { - const last = this.tmp(); - this.line(`${last} = add i32 ${i16}, ${reach}`); - this.line(`${fits} = icmp sle i32 ${last}, %len`); - } + // The characters left (0 <= i <= len, so this cannot wrap; i + 16 + // can for subjects near 2^31 characters). + this.line(`${rest} = sub i32 %len, ${i}`); + this.line(`${fits} = icmp sge i32 ${rest}, ${16 + reach}`); this.line(`br i1 ${fits}, label %${load}, label %${tail}`); this.block(load); const mask = filter !== null ? this.testPrefix(filter, i) : this.testVector(plan!, i), @@ -899,6 +896,7 @@ class RxGen { this.line(`${any} = icmp ne i16 ${mask}, 0`); this.line(`br i1 ${any}, label %${hit}, label %${next}`); this.block(next); + this.line(`${i16} = add i32 ${i}, 16`); this.line(`br label %${head}`); this.block(hit); const k = this.lowestBit(mask), @@ -1149,10 +1147,10 @@ class RxGen { check = this.label("chk"); this.block(entry); const p = this.loadPos(), - end = this.tmp(), + rest = this.tmp(), inb = this.tmp(); - this.line(`${end} = add i32 ${p}, ${sets.length}`); - this.line(`${inb} = icmp sle i32 ${end}, %len`); + this.line(`${rest} = sub i32 %len, ${p}`); + this.line(`${inb} = icmp sge i32 ${rest}, ${sets.length}`); this.line(`br i1 ${inb}, label %${check}, label %fail`); this.block(check); let acc = ""; @@ -1171,6 +1169,8 @@ class RxGen { acc = both; } } + const end = this.tmp(); + this.line(`${end} = add i32 ${p}, ${sets.length}`); // Storing before the branch is safe: every resume sets the position. this.storePos(end); this.line(`br i1 ${acc}, label %${succ}, label %fail`); @@ -1377,10 +1377,11 @@ class RxGen { this.block(vhead); const kv = this.tmp(), kv16 = this.tmp(), + left = this.tmp(), fits = this.tmp(); this.line(`${kv} = phi i32 [ 0, %${entry} ], [ ${kv16}, %${vnext} ]`); - this.line(`${kv16} = add i32 ${kv}, 16`); - this.line(`${fits} = icmp sle i32 ${kv16}, ${lim}`); + this.line(`${left} = sub i32 ${lim}, ${kv}`); + this.line(`${fits} = icmp sge i32 ${left}, 16`); this.line(`br i1 ${fits}, label %${vload}, label %${tail}`); this.block(vload); const vat = this.tmp(); @@ -1390,6 +1391,7 @@ class RxGen { this.line(`${all} = icmp eq i16 ${mask}, -1`); this.line(`br i1 ${all}, label %${vnext}, label %${vexit}`); this.block(vnext); + this.line(`${kv16} = add i32 ${kv}, 16`); this.line(`br label %${vhead}`); this.block(vexit); const missing = this.tmp(); diff --git a/packages/compiler/test/regex-matcher-bounds.test.ts b/packages/compiler/test/regex-matcher-bounds.test.ts new file mode 100644 index 0000000000..3e0ec7f1bb --- /dev/null +++ b/packages/compiler/test/regex-matcher-bounds.test.ts @@ -0,0 +1,91 @@ +/* Compiled regex matchers bound every read by the characters left in the + * subject, so subjects near the runtime's 2^31 - 2 character limit are + * read only inside the subject. Each matcher runs on a subject of exactly + * that length whose last byte sits just before an inaccessible page: a + * bounds check that wraps past INT32_MAX reads that page and faults. The + * subject is a lazily mapped zero region (no memory is committed for it) + * with the characters each case needs written near its end. */ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compileRegexMatcher, REGEX_MATCHER_DECLS } from "../src/backend/llvm/regex-matchers.js"; + +const LIMIT = 2 ** 31 - 2; + +/** [symbol, pattern, flags, start offset from the end, tail text]. */ +const CASES: [string, string, string, number, string][] = [ + // The first-character vector search, 10 characters before the end. + ["seek", "[b-d]", "", 10, "c"], + // The start filter (two 8-character alternatives), 20 before the end. + ["filter", "agggtaaa|tttaccct", "i", 20, "TTTACCCT"], + // A run of 8 literal characters beginning 2 before the end. + ["run", "xy234567", "", 30, "xy"], + // A greedy class over the whole subject, 16 characters per step. + ["greedy", "[^x]+", "", LIMIT, ""], +]; + +const DRIVER = ` +#include +#include +#include +#include +#include +void scr_regex_stack_grow(void *a, void *b, void *c, int32_t d) { (void)a; (void)b; (void)c; (void)d; _exit(3); } +void scr_regex_stack_free(void *p) { (void)p; } +${CASES.map(([sym]) => `int32_t ${sym}(const char *, int32_t, int32_t, const char **);`).join("\n")} +static void check(const char *name, int32_t (*m)(const char *, int32_t, int32_t, const char **), + int32_t start, const char *tail) { + const int32_t len = ${LIMIT}; + size_t page = (size_t)sysconf(_SC_PAGESIZE); + size_t body = ((size_t)len + page - 1) / page * page; + char *base = mmap(NULL, body + page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + if (base == MAP_FAILED || mprotect(base + body, page, PROT_NONE) != 0) _exit(4); + char *s = base + (body - (size_t)len); + memcpy(s + len - strlen(tail), tail, strlen(tail)); + const char *out[16] = {0}; + int32_t found = m(s, len, start, out); + if (found) printf("%s %ld %ld\\n", name, (long)(out[0] - s), (long)(out[1] - s)); + else printf("%s none\\n", name); + fflush(stdout); + munmap(base, body + page); +} +int main(void) { +${CASES.map(([sym, , , back, tail]) => ` check("${sym}", ${sym}, ${LIMIT - back}, ${JSON.stringify(tail)});`).join("\n")} + return 0; +} +`; + +test.skipIf(process.platform === "win32")( + "compiled regex matchers stay inside subjects near 2^31 characters", + { timeout: 120_000 }, + () => { + const lines: string[] = []; + const decls = new Set(REGEX_MATCHER_DECLS); + for (const [sym, pattern, flags] of CASES) { + const code = compileRegexMatcher(sym, pattern, flags, "i64", true); + if (code === null) throw new Error(`/${pattern}/${flags} did not compile`); + lines.push(...code.lines.map((line) => line.replace(/^define internal /, "define "))); + for (const decl of code.decls) decls.add(decl); + } + const dir = mkdtempSync(join(tmpdir(), "scriptc-regex-bounds-")); + try { + const ir = join(dir, "matchers.ll"); + const driver = join(dir, "driver.c"); + const binary = join(dir, "bounds"); + writeFileSync(ir, [...decls, ...lines, "attributes #0 = { nounwind }", ""].join("\n")); + writeFileSync(driver, DRIVER); + execFileSync("clang", ["-O2", "-Wno-override-module", ir, driver, "-o", binary]); + const output = execFileSync(binary, { encoding: "utf8" }); + expect(output.trim().split("\n")).toEqual([ + `seek ${LIMIT - 1} ${LIMIT}`, + `filter ${LIMIT - 8} ${LIMIT}`, + "run none", + `greedy 0 ${LIMIT}`, + ]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }, +); From 866c0bfea5a22bff19537bf33f8c375fc7d8823c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:05:45 -0500 Subject: [PATCH 151/395] Version typed-array loops whose index adds an unranged invariant term A row-major dot product, `const base = r * cols; for (let j = 0; j < cols; j++) s += w[base + j] * v[j]`, kept one index test per `w` read: the typed-array loop version guard covered an index `k * counter + terms + c` only when every loop-invariant term had a proven integer range, and `base` has none when `cols` is a parameter. The guard now also accepts a pure loop-invariant term without a proven range. It evaluates the term once and requires an exact integer (not -0) between -2^32 and 2^32, then checks the first and last index as for any affine index. In the fast copy the term has that range, so the index is integer arithmetic and the reads take no index test. Any other value runs the checked copy. Adds a Float32Array matrix-vector product with four accumulators per row to the typed-array-kernels workload. --- benchmarks/runtime/README.md | 2 +- .../runtime/workloads/typed-array-kernels.ts | 31 ++++++- .../backend/llvm/bytes-loop-versions.test.ts | 33 +++++++ .../src/backend/llvm/bytes-loop-versions.ts | 89 ++++++++++++++++--- ...-typed-array-invariant-index-terms.ts.json | 6 ++ .../6997-typed-array-invariant-index-terms.ts | 78 ++++++++++++++++ ...-typed-array-invariant-index-terms.ts.json | 1 + 7 files changed, 227 insertions(+), 13 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6997-typed-array-invariant-index-terms.ts.json create mode 100644 tests/corpus/6997-typed-array-invariant-index-terms.ts create mode 100644 tests/harness/corpus-costs/6997-typed-array-invariant-index-terms.ts.json diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index cbf47675ad..1e01aa3a75 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -26,7 +26,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds | | `template-render` | cpu | template literals, escaping, `map`/`join` | | `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread | -| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray | +| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, and a row-major `Float32Array` matrix-vector product | | `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage | | `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts | | `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains | diff --git a/benchmarks/runtime/workloads/typed-array-kernels.ts b/benchmarks/runtime/workloads/typed-array-kernels.ts index ba68f7241b..4fd2e7cd70 100644 --- a/benchmarks/runtime/workloads/typed-array-kernels.ts +++ b/benchmarks/runtime/workloads/typed-array-kernels.ts @@ -1,5 +1,6 @@ // Element-wise loops over several typed arrays: each loop runs to one -// array's length and reads or writes the others at the same index. +// array's length and reads or writes the others at the same index. Also a +// row-major matrix-vector product with four accumulators per row. function axpy(a: number, x: Float64Array, y: Float64Array, out: Float64Array): void { for (let i = 0; i < out.length; i++) out[i] = a * x[i]! + y[i]!; @@ -21,6 +22,23 @@ function brighten(src: Uint8ClampedArray, gain: number, out: Uint8ClampedArray): for (let i = 0; i < out.length; i++) out[i] = src[i]! * gain + 8; } +function matvec(w: Float32Array, x: Float32Array, out: Float32Array, cols: number): void { + for (let r = 0; r < out.length; r++) { + const base = r * cols; + let s0 = 0; + let s1 = 0; + let s2 = 0; + let s3 = 0; + for (let j = 0; j < cols; j += 4) { + s0 += w[base + j]! * x[j]!; + s1 += w[base + j + 1]! * x[j + 1]!; + s2 += w[base + j + 2]! * x[j + 2]!; + s3 += w[base + j + 3]! * x[j + 3]!; + } + out[r] = s0 + s1 + s2 + s3; + } +} + function gray(rgba: Uint8Array, out: Uint8Array): void { for (let i = 0; i < out.length; i++) { const j = i * 4; @@ -45,6 +63,10 @@ const io = new Int32Array(n); const pixels = new Uint8ClampedArray(n); const bright = new Uint8ClampedArray(n); const rgba = new Uint8Array(n * 4); +const side = 256; +const weights = new Float32Array(side * side); +const vector = new Float32Array(side); +const product = new Float32Array(side); const luma = new Uint8Array(n); let state = 2463534242; const next = (): number => { @@ -65,6 +87,8 @@ for (let i = 0; i < n; i++) { pixels[i] = next() & 255; } for (let i = 0; i < rgba.length; i++) rgba[i] = next() & 255; +for (let i = 0; i < weights.length; i++) weights[i] = (next() % 2001) / 1000 - 1; +for (let i = 0; i < side; i++) vector[i] = (next() % 1001) / 1000; const rounds = Math.round(1200 * scale); let f64Sum = 0; @@ -73,6 +97,7 @@ let byteSum = 0; let intSum = 0; let brightSum = 0; let lumaSum = 0; +let productSum = 0; for (let r = 0; r < rounds; r++) { // Change one input element per round so no round repeats the last. const at = (r * 7919) & (n - 1); @@ -82,12 +107,14 @@ for (let r = 0; r < rounds; r++) { ia[at] = r * 65537; pixels[at] = r & 255; rgba[at * 4] = r & 255; + vector[r & (side - 1)] = (r % 1001) / 1000; axpy(1.5, x, y, d); blend((r & 15) / 16, fa, fb, fo); mask(data, key, masked); addWrap(ia, ib, io); brighten(pixels, 0.75 + (r & 7) / 8, bright); gray(rgba, luma); + matvec(weights, vector, product, side); const probe = (r * 104729) & (n - 1); f64Sum += d[probe]!; f32Sum += fo[probe]!; @@ -95,6 +122,7 @@ for (let r = 0; r < rounds; r++) { intSum = (intSum + io[probe]!) | 0; brightSum += bright[probe]!; lumaSum += luma[probe]!; + productSum += product[probe & (side - 1)]!; } console.log(`float64 ${f64Sum.toFixed(3)}`); console.log(`float32 ${f32Sum.toFixed(3)}`); @@ -102,3 +130,4 @@ console.log(`uint8 ${byteSum}`); console.log(`int32 ${intSum}`); console.log(`clamped ${brightSum}`); console.log(`gray ${lumaSum}`); +console.log(`matvec ${productSum.toFixed(3)}`); 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 28194c7b34..ace8f294ce 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -134,6 +134,39 @@ test("a counted loop's affine accesses get one guard and exact operand ranges", expect(version.ranges.get(shifted)).toEqual({ min: 1, max: Number.MAX_SAFE_INTEGER }); }); +test("invariant terms without a range are tested by the guard", () => { + // for (let i = 0; i < n; i++) out[i] = src[k + i], with `k` a parameter + const shifted = bin("+", ref("k"), ref("i")); + const loop = counted([set(ref("out", f64s), ref("i"), get(ref("src", f64s), shifted))]); + const kernel = fn([loop]); + const version = match(kernel, loop)!; + expect([...version.guarded]).toEqual(["v1:k"]); + expect(version.affine.map((c) => c.terms.map((t) => t.key))).toEqual([[], ["v1:k"]]); + // In the fast copy the term has the range the guard tests. + expect(version.ranges.get(shifted.kind === "bin" ? shifted.left : shifted)).toEqual({ + min: -(2 ** 32), + max: 2 ** 32, + }); + 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\.(fast|slow)\d+:$/m)); + // -2^32 <= k <= 2^32, and k is the double of its frozen integer. + expect(guard).toMatch(/fcmp oge double %\S+, 0xC1F0000000000000/); + expect(guard).toMatch(/fcmp ole double %\S+, 0x41F0000000000000/); + expect(guard).toMatch(/freeze i64/); + 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("statically ranged indices compare one constant with the length", () => { const masked = bin("&", bin("^", ref("i"), ref("k")), num(255)); const loop = counted([set(ref("out", f64s), num(0), get(ref("table", i32s), masked))]); diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index 8c7f668a00..c3cee3feb5 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -7,6 +7,7 @@ import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; 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"; type Node = IrExpr | IrStmt; type VarRef = IrExpr & { kind: "varRef" }; @@ -50,6 +51,9 @@ export interface BytesLoopVersion { ownedReceivers: number[]; fixed: FixedCheck[]; affine: AffineCheck[]; + /** Keys of affine terms the guard tests at run time: exact integers + * within GUARDED_TERM. */ + guarded: ReadonlySet; /** The counted loop whose iteration space the affine checks cover. */ counted: IntegerCountedForLoop | null; /** The guard also requires every counter value within [-bound, bound]. */ @@ -83,6 +87,9 @@ const MAX_FIXED_END = 2 ** 32; const MAX_CHECKS = 8; const MAX_OFFSET = 2 ** 31; const SAFE = Number.MAX_SAFE_INTEGER; +/** The range the guard requires of an invariant index term without a + * proven one: every index of a typed array of at most 2^32 elements. */ +const GUARDED_TERM: IntegerRange = { min: -(2 ** 32), max: 2 ** 32 }; const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]); /** Loop versioning for typed-array bounds. A typed array's length and data @@ -95,7 +102,9 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]) * `hi < length`, such as `table[x & 0xff]` or a constant-trip counter; * - an index `k * counter + invariant + c` of a counted loop needs its first * and last values in bounds, such as `out[base + i]` for `i < n` or - * `view.getUint32(i * 4)`. + * `view.getUint32(i * 4)`. An invariant without a proven integer range, + * such as `base` for `const base = r * cols`, must also be an exact + * integer between -2^32 and 2^32, which the guard tests. * * 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 @@ -198,9 +207,8 @@ export function matchBytesLoopVersion( } return invariant(e.localId) ? e : null; }; - // A pure integer operand evaluated again before the loop. - const invariantKey = (e: IrExpr): string | null => { - if (!facts.ranges.get(e)) return null; + // A pure loop-invariant number, evaluated again before the loop. + const pureKey = (e: IrExpr): string | null => { if (e.kind === "numLit") return Number.isSafeInteger(e.value) ? `n${e.value}` : null; if (e.kind === "varRef") return invariant(e.localId) ? `v${e.localId.length}:${e.localId}` : null; @@ -210,12 +218,27 @@ export function matchBytesLoopVersion( e.left.type.kind === "f64" && e.right.type.kind === "f64" ) { - const left = invariantKey(e.left), - right = invariantKey(e.right); + const left = pureKey(e.left), + right = pureKey(e.right); return left !== null && right !== null ? `(${e.op} ${left} ${right})` : null; } return null; }; + // Invariant terms without a proven range, such as `base` in + // `w[base + j]` with `const base = r * cols` for a parameter `cols`. The + // guard tests each one: an exact integer (not -0) within GUARDED_TERM. + const guardedRanges = new Map(); + const guardedNodes = new Map(); + // The key of a loop-invariant index term, with a proven range or guarded. + const invariantKey = (e: IrExpr): string | null => { + const key = pureKey(e); + if (key === null || facts.ranges.get(e)) return key; + if (e.kind === "numLit") return null; + guardedRanges.set(e, GUARDED_TERM); + guardedNodes.set(e, key); + return key; + }; + const termRange = (e: IrExpr): IntegerRange => facts.ranges.get(e) ?? guardedRanges.get(e)!; interface Affine { /** The counter's coefficient; 0 when the value is loop-invariant. */ counter: number; @@ -223,9 +246,10 @@ export function matchBytesLoopVersion( offset: number; } // Every operand of an affine index is `counter * k + Σ terms + offset`. - // Invariant terms need proven ranges. Operands that involve the counter - // need none: the guard bounds the counter so that each of them is a safe - // integer, which makes the index's double arithmetic exact. + // Invariant terms need proven ranges or the guard's test (guarded terms). + // Operands that involve the counter need none: the guard bounds the + // counter so that each of them is a safe integer, which makes the index's + // double arithmetic exact. const shapes = new Map(); const affine = (e: IrExpr): Affine | null => { if (shapes.has(e)) return shapes.get(e)!; @@ -309,7 +333,7 @@ export function matchBytesLoopVersion( }; const termMagnitude = (terms: Term[]): number => terms.reduce((sum, t) => { - const r = facts.ranges.get(t.expr)!; + const r = termRange(t.expr); return sum + Math.max(Math.abs(r.min), Math.abs(r.max)); }, 0); @@ -433,7 +457,7 @@ export function matchBytesLoopVersion( let min = Math.min(a, b) + shape.offset, max = Math.max(a, b) + shape.offset; for (const term of shape.terms) { - const r = facts.ranges.get(term.expr)!; + const r = termRange(term.expr); min += term.sign > 0 ? r.min : -r.max; max += term.sign > 0 ? r.max : -r.min; } @@ -447,6 +471,12 @@ export function matchBytesLoopVersion( ranges.set(operand, { min: Math.max(min, known.min), max: Math.min(max, known.max) }); } } + // Guarded terms of the kept checks, and the range the guard proves for + // every node of each in the fast copy. + const guarded = new Set(); + 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); // 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])); @@ -470,6 +500,7 @@ export function matchBytesLoopVersion( minOffset: g.check.minOffset, maxEnd: g.check.maxEnd, })), + guarded, counted: affineKept.length > 0 ? counted : null, counterBound: needsBound ? bound : null, ranges, @@ -567,6 +598,14 @@ export function emitBytesLoopVersionGuard( let base = "0"; for (const term of check.terms) { const value = host.emitExpr(term.expr); + if (version.guarded.has(term.key)) { + const tested = emitGuardedTerm(host, value.name); + conditions.push(tested.ok); + const sum = B.tmp(); + B.line(`${sum} = ${term.sign > 0 ? "add" : "sub"} i64 ${base}, ${tested.integer}`); + base = sum; + continue; + } const integer = exactInteger(host, value, term.expr); if (!integer) { // Unreachable: every term has a proven range. Stay checked. @@ -614,6 +653,34 @@ 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 } { + const B = host.B; + const low = B.tmp(), + high = B.tmp(), + inRange = B.tmp(), + raw = B.tmp(), + integer = B.tmp(), + back = B.tmp(), + bits = B.tmp(), + 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(`${inRange} = and i1 ${low}, ${high}`); + B.line(`${raw} = fptosi double ${value} to i64`); + B.line(`${integer} = freeze i64 ${raw}`); + B.line(`${back} = sitofp i64 ${integer} to double`); + B.line(`${bits} = bitcast double ${value} to i64`); + B.line(`${backBits} = bitcast double ${back} to i64`); + B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); + B.line(`${ok} = and i1 ${inRange}, ${exact}`); + return { ok, integer }; +} + function scaleCounter(host: LlvmEmitterContext, value: string, scale: number): string { if (scale === 1) return value; const scaled = host.B.tmp(); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6997-typed-array-invariant-index-terms.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6997-typed-array-invariant-index-terms.ts.json new file mode 100644 index 0000000000..6d60d2635a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6997-typed-array-invariant-index-terms.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6997-typed-array-invariant-index-terms.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6997-typed-array-invariant-index-terms.ts b/tests/corpus/6997-typed-array-invariant-index-terms.ts new file mode 100644 index 0000000000..53d9f10c0c --- /dev/null +++ b/tests/corpus/6997-typed-array-invariant-index-terms.ts @@ -0,0 +1,78 @@ +// Typed-array loops whose index adds a loop-invariant number with no known +// integer range, such as the row base of a matrix-vector product. Rows +// with integer, fractional, negative, -0 and NaN bases, windows at 2^32 and +// beyond, and matrices too short for their last rows, read and write as +// Node does. + +function matvec(w: Float32Array, v: Float32Array, out: Float32Array, cols: number): void { + for (let r = 0; r < out.length; r++) { + const base = r * cols; + let s0 = 0, s1 = 0, s2 = 0, s3 = 0; + for (let j = 0; j < cols; j += 4) { + s0 += w[base + j]! * v[j]!; + s1 += w[base + j + 1]! * v[j + 1]!; + s2 += w[base + j + 2]! * v[j + 2]!; + s3 += w[base + j + 3]! * v[j + 3]!; + } + out[r] = s0 + s1 + s2 + s3; + } +} + +function rowSums(m: Float64Array, rows: number, cols: number, first: number): number[] { + const sums: number[] = []; + for (let r = first; r < first + rows; r++) { + let s = 0; + for (let c = 0; c < cols; c++) s += m[r * cols + c]!; + sums.push(s); + } + return sums; +} + +function shiftedCopy(src: Int16Array, out: Int16Array, offset: number): void { + for (let i = 0; i < out.length; i++) out[i] = src[i - offset]! + 1; +} + +function window(src: Uint8Array, start: number, k: number): number { + let s = 0; + for (let i = 0; i < k; i++) s += src[start + i]! * (i + 1); + return s; +} + +const rows = 6, cols = 8; +const w = new Float32Array(rows * cols); +for (let i = 0; i < w.length; i++) w[i] = ((i * 37) % 23) / 4 - 2; +const v = new Float32Array(cols); +for (let i = 0; i < cols; i++) v[i] = (i % 5) - 1.5; +const out = new Float32Array(rows); +matvec(w, v, out, cols); +console.log("matvec", out.join(",")); +const tall = new Float32Array(rows + 2); +matvec(w, v, tall, cols); +console.log("matvec past rows", tall.join(",")); +const odd = new Float32Array(3); +matvec(w, v, odd, 6.5); +console.log("matvec fractional cols", odd.join(",")); +matvec(w, v, odd, NaN); +console.log("matvec NaN cols", odd.join(",")); + +const m = new Float64Array(30); +for (let i = 0; i < m.length; i++) m[i] = i * 1.5 - 7; +console.log("rows", rowSums(m, 5, 6, 0).join(",")); +console.log("rows from -1", rowSums(m, 3, 6, -1).join(",")); +console.log("rows fractional", rowSums(m, 2, 2.5, 1).join(",")); +console.log("rows past end", rowSums(m, 3, 6, 4).join(",")); +console.log("rows -0 base", rowSums(m, 1, 6, -0).join(",")); + +const src16 = new Int16Array([10, 20, 30, 40, 50, 60]); +const shifted = new Int16Array(6); +shiftedCopy(src16, shifted, 2); +console.log("shifted", shifted.join(",")); +shiftedCopy(src16, shifted, -3); +console.log("shifted back", shifted.join(",")); +shiftedCopy(src16, shifted, 0.5); +console.log("shifted half", shifted.join(",")); + +const bytes = new Uint8Array(40); +for (let i = 0; i < bytes.length; i++) bytes[i] = (i * 29) & 255; +console.log("window", window(bytes, 3, 8), window(bytes, 36, 8), window(bytes, -2, 4)); +console.log("window far", window(bytes, 2 ** 32, 3), window(bytes, 2 ** 32 + 1, 3), window(bytes, 2 ** 40, 2), window(bytes, -(2 ** 32), 2)); diff --git a/tests/harness/corpus-costs/6997-typed-array-invariant-index-terms.ts.json b/tests/harness/corpus-costs/6997-typed-array-invariant-index-terms.ts.json new file mode 100644 index 0000000000..ed05a3c5d5 --- /dev/null +++ b/tests/harness/corpus-costs/6997-typed-array-invariant-index-terms.ts.json @@ -0,0 +1 @@ +{ "plain": 3400, "san": 4800 } From e4a9940e5b4d45170992c3094cd7888dbf064fdb Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:07:12 -0500 Subject: [PATCH 152/395] Finish bailed speculative iterations outside the ordinary loop copy A speculative integer loop's failed check used to continue at the failed statement inside the ordinary copy. That gave the ordinary copy a second entry, so LLVM saw an irreducible cycle and did not hoist its invariant loads or keep its values in registers. A loop whose check fails on every entry, such as an inner loop over fractional fields, then ran every iteration in that unoptimized copy. A failed check now continues in a third copy that is entered only from the fast copy: it re-executes the failed statement, the rest of the body and the update, then jumps to the ordinary copy's header. The ordinary copy has a single entry and stays a natural loop. Each loop site also counts its entries in a row whose fast copy bailed. A fast copy that completes a speculated write and leaves the loop clears the count, and after 8 bailing entries in a row the site runs the ordinary copy only. The count is a hint, read and written with unordered atomics: both copies give the same results. --- .../src/backend/llvm/emit-statements.ts | 100 ++++++++++++++-- packages/compiler/src/backend/llvm/emitter.ts | 11 ++ .../test/speculative-loop-emission.test.ts | 85 ++++++++++++++ .../7321-speculative-loop-bail-sites.ts.json | 6 + .../7321-speculative-loop-bail-sites.ts | 111 ++++++++++++++++++ .../7321-speculative-loop-bail-sites.ts.json | 1 + 6 files changed, 304 insertions(+), 10 deletions(-) create mode 100644 packages/compiler/test/speculative-loop-emission.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7321-speculative-loop-bail-sites.ts.json create mode 100644 tests/corpus/7321-speculative-loop-bail-sites.ts create mode 100644 tests/harness/corpus-costs/7321-speculative-loop-bail-sites.ts.json diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 7239db7d89..a22de3445d 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -803,6 +803,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const lp = workerBudget !== null ? B.newLabel("loop.p") : lc; B.br(lc); B.startBlock(lc); + enterLoopHeader(emitter, lc); B.condBr(emitter.emitCondition(s.cond), lb, exit); B.startBlock(lb); emitter.jumpTargets.push({ @@ -835,6 +836,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { const lc = B.newLabel("loop.c"); B.br(lb); B.startBlock(lb); + enterLoopHeader(emitter, lb); emitter.jumpTargets.push({ kind: "loop", brkLabel: exit, @@ -1135,6 +1137,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { s.update || capturedLets.length > 0 || workerBudget !== null ? B.newLabel("loop.u") : lc; B.br(lc); B.startBlock(lc); + enterLoopHeader(emitter, lc); if (integerLoop && integerSlot) { const receiver = emitter.emitStableReceiver(integerLoop.limitReceiver, []); const lenPtr = B.tmp(); @@ -1885,6 +1888,10 @@ export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }): B.startBlock(end); } +/** Entries in a row whose fast copy bailed before a speculative loop site + * stops trying it. */ +const SPECULATION_BAILS = 8; + /** Emit a loop's condition, body and update through `emitCopy`, twice * 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 @@ -1939,23 +1946,66 @@ export function emitLoopCopies( ok = both; } } + // Each entry whose fast copy bails counts at the loop site, and a fast + // copy that completes a speculated write and leaves the loop clears the + // 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 count = B.tmp(), + trying = B.tmp(); + B.line(`${count} = load atomic i8, ptr ${bails} unordered, align 1`); + B.line(`${trying} = icmp ult i8 ${count}, ${SPECULATION_BAILS}`); + if (ok === "true") ok = trying; + else { + const both = B.tmp(); + B.line(`${both} = and i1 ${ok}, ${trying}`); + ok = both; + } const fast = B.newLabel("spec.fast"), slow = B.newLabel("spec.slow"); B.condBr(ok, fast, slow); const resume = new Map(); for (const s of plan.writes) resume.set(s, { label: B.newLabel("spec.resume"), placed: false }); + // A failed check finishes its iteration in a third copy, entered only + // there: it re-executes the statement and the rest of the body and the + // update, then enters the ordinary copy through its header. Jumping + // into the middle of the ordinary copy instead would give that loop a + // second entry, and LLVM does not optimize an irreducible loop. + const points = [...resume.values()]; + let header: string | null = null; B.startBlock(slow); + emitter.speculationHeader = (label) => { + header = label; + }; + try { + emitCopy(exit); + } finally { + emitter.speculationHeader = null; + } + if (header === null) + throw new InternalCompilerError("llvm emitter bug: speculative loop copy without a header"); + B.startBlock(B.newLabel("spec.tail")); emitter.speculationResume = resume; + emitter.speculationHeader = () => { + B.br(header!); + B.startBlock(B.newLabel("spec.unused")); + }; try { emitCopy(exit); } finally { emitter.speculationResume = null; + emitter.speculationHeader = null; } B.startBlock(fast); - if (![...resume.values()].every((r) => r.placed)) { + if (!points.every((r) => r.placed)) { B.br(slow); return; } + const ran = B.slot(); + B.entryAllocas.push(`${ran} = alloca i1 ; speculated write completed`); + B.line(`store i1 false, ptr ${ran}`); for (const id of plan.locals.keys()) { const shadow = B.slot(); B.entryAllocas.push( @@ -1992,22 +2042,43 @@ export function emitLoopCopies( writes: plan.writes, resume: new Map([...resume].map(([s, r]) => [s, r.label])), deferred, + bails, + ran, }; try { - if (deferred.size === 0) emitCopy(exit); - else { - const leave = B.newLabel("spec.leave"); - emitCopy(leave); - B.startBlock(leave); - emitter.writeBackDeferred(deferred); - B.br(exit); - } + const leave = B.newLabel("spec.leave"); + emitCopy(leave); + B.startBlock(leave); + emitter.writeBackDeferred(deferred); + // An entry that never ran a speculated write (a loop of no iterations) + // says nothing about the site. + const wrote = B.tmp(), + left = B.tmp(), + counted = B.tmp(), + both = B.tmp(), + clear = B.newLabel("spec.clear"); + B.line(`${wrote} = load i1, ptr ${ran}`); + B.line(`${left} = load atomic i8, ptr ${bails} unordered, align 1`); + B.line(`${counted} = icmp ne i8 ${left}, 0`); + B.line(`${both} = and i1 ${wrote}, ${counted}`); + B.condBr(both, clear, exit); + B.startBlock(clear); + B.line(`store atomic i8 0, ptr ${bails} unordered, align 1`); + B.br(exit); } finally { emitter.speculation = null; for (const id of plan.locals.keys()) emitter.integerLoopBindings.delete(id); } } +/** At the top of a loop copy's header: lets a speculative loop find its + * ordinary copy's header, and keeps its third copy out of the loop. */ +function enterLoopHeader(emitter: LlEmitter, header: string): void { + const dispatch = emitter.speculationHeader; + emitter.speculationHeader = null; + dispatch?.(header); +} + /** A proven integer loop (speculative-integers.ts) for this loop, or null. * A shadowed local's double is stale until the loop is left, which only * the loop's exit, a return and an exception do: the loop is planned only @@ -2134,7 +2205,8 @@ export function emitSpeculativeWrite( if (emitter.frames.pop()!.length > 0) throw new InternalCompilerError("llvm emitter bug: speculative write owns a temporary"); B.line(`store i64 ${result}, ptr ${binding.slot}`); - const deferred = emitter.speculation!.deferred; + const { deferred, bails: site, ran } = emitter.speculation!; + if (ran !== undefined) B.line(`store i1 true, ptr ${ran}`); if (!deferred.has(target.localId)) { const number = B.tmp(); B.line(`${number} = sitofp i64 ${result} to double`); @@ -2145,6 +2217,14 @@ export function emitSpeculativeWrite( B.br(done); B.startBlock(failed); emitter.writeBackDeferred(deferred); + if (site !== undefined) { + const count = B.tmp(), + next = B.tmp(); + emitter.declare("declare i8 @llvm.uadd.sat.i8(i8, i8)"); + B.line(`${count} = load atomic i8, ptr ${site} unordered, align 1`); + B.line(`${next} = call i8 @llvm.uadd.sat.i8(i8 ${count}, i8 1)`); + B.line(`store atomic i8 ${next}, ptr ${site} unordered, align 1`); + } B.br(resume!); B.startBlock(done); } diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index ed17859126..c1cf66af44 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1102,9 +1102,20 @@ export class LlEmitter { resume: Map; /** Globals whose double is written back only when the copy is left. */ deferred: Set; + /** The loop site's count of bailing entries in a row (a global), and + * whether this entry completed a speculated write (a slot). A proven + * integer loop has neither. */ + bails?: string; + ran?: string; } | null = null; + /** Speculative loop sites numbered so far in the module. */ + speculativeLoopSites = 0; /** The ordinary copy being emitted: resume labels still to place. */ speculationResume: Map | null = null; + /** Run once at the top of the next loop copy's header + * (`enterLoopHeader`): records the ordinary copy's header, or sends the + * third copy's header there. */ + speculationHeader: ((header: string) => void) | null = null; /** The function being emitted, for speculative globals. */ speculativeFunction = ""; /** Every function that writes each number global (computed on demand). */ diff --git a/packages/compiler/test/speculative-loop-emission.test.ts b/packages/compiler/test/speculative-loop-emission.test.ts new file mode 100644 index 0000000000..78a7de295f --- /dev/null +++ b/packages/compiler/test/speculative-loop-emission.test.ts @@ -0,0 +1,85 @@ +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-speculative-loop-")); + 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 = ` +class Point { + x: number; + w = 0; + constructor(x: number) { + this.x = x; + } +} +function scaled(points: Point[], scale: number): number { + let s = 0; + for (let i = 0; i < points.length; i++) { + const p = points[i]!; + p.w += 1; + const k = p.x * scale; + s += k * k; + } + return s; +} +console.log(scaled([new Point(1), new Point(2)], 2), scaled([new Point(1)], 0.5)); +`; + +function labels(fn: string): string[] { + return [...fn.matchAll(/^([\w.]+):/gm)].map((m) => m[1]!); +} + +test("a failed check finishes its iteration outside the ordinary copy", async () => { + const llvm = await emitLlvm(source); + const fn = /^define internal double @sc_bf_scaled\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + const order = labels(fn); + const slow = order.findIndex((l) => l.startsWith("spec.slow")); + const tail = order.findIndex((l) => l.startsWith("spec.tail")); + const fast = order.findIndex((l) => l.startsWith("spec.fast")); + expect(slow).toBeGreaterThanOrEqual(0); + expect(tail).toBeGreaterThan(slow); + expect(fast).toBeGreaterThan(tail); + // Every resume point is in the third copy, between its start and the + // fast copy, so the ordinary copy has one entry. + const resumes = order.flatMap((l, i) => (l.startsWith("spec.resume") ? [i] : [])); + expect(resumes.length).toBeGreaterThan(0); + for (const i of resumes) { + expect(i).toBeGreaterThan(tail); + expect(i).toBeLessThan(fast); + } + // The third copy's header leaves for the ordinary copy's header. + const header = order[slow + 1]!; + const third = fn.slice(fn.indexOf(`\n${order[tail]}:`), fn.indexOf(`\n${order[fast]}:`)); + expect(third).toContain(`br label %${header}`); +}); + +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]; + 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`, + ), + ); + expect(fn).toMatch(new RegExp(`call i8 @llvm\\.uadd\\.sat\\.i8\\(i8 %\\S+, i8 1\\)`)); + expect(fn).toContain(`store atomic i8 0, ptr ${site} unordered, align 1`); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7321-speculative-loop-bail-sites.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7321-speculative-loop-bail-sites.ts.json new file mode 100644 index 0000000000..113eeedb79 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7321-speculative-loop-bail-sites.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7321-speculative-loop-bail-sites.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7321-speculative-loop-bail-sites.ts b/tests/corpus/7321-speculative-loop-bail-sites.ts new file mode 100644 index 0000000000..460595febf --- /dev/null +++ b/tests/corpus/7321-speculative-loop-bail-sites.ts @@ -0,0 +1,111 @@ +// Speculative integer loops whose checks fail: on the first statement of +// every entry (fractional field data), after an effect earlier in the same +// iteration, before a `continue`, `break` or `return`, and on entries +// that alternate between integer and fractional data. A site that keeps +// failing stops trying its integer copy; every result is Node's. + +class Point { + x: number; + y: number; + w = 0; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +function show(label: string, value: number): void { + console.log(label, Object.is(value, -0) ? "-0" : String(value)); +} + +// Each inner entry fails at its first statement when the fields hold +// fractions, and runs no iteration at all for the last point. +function pairs(points: Point[]): number { + let total = 0; + for (let i = 0; i < points.length; i++) { + const p = points[i]!; + let acc = p.w; + for (let j = i + 1; j < points.length; j++) { + const q = points[j]!; + const dx = p.x - q.x; + const dy = p.y - q.y; + const d2 = dx * dx + dy * dy; + acc += d2; + q.w += d2 * 0.5; + } + p.w = acc; + total += acc; + } + return total; +} + +const fractional = [ + new Point(0.5, 1), + new Point(1.25, -2), + new Point(3, 4.75), + new Point(-1.5, 0), +]; +const integral = [new Point(0, 1), new Point(2, -2), new Point(3, 4), new Point(-1, 0)]; +for (let round = 0; round < 12; round++) { + show(`pairs fractional ${round}`, pairs(fractional)); +} +// The same site, now with integer data after it stopped speculating. +show("pairs integral", pairs(integral)); +show("pairs integral again", pairs(integral)); + +// A field write comes before the failing statement: the iteration resumes +// after it and must not repeat it. +function afterEffect(points: Point[], scale: number): number { + let s = 0; + for (let i = 0; i < points.length; i++) { + const p = points[i]!; + p.w += 1; + const k = p.x * scale; + s += k * k; + } + return s; +} +const marks = [new Point(1, 0), new Point(2, 0), new Point(3, 0)]; +show("after effect 2", afterEffect(marks, 2)); +show("after effect 0.5", afterEffect(marks, 0.5)); +show("after effect 3", afterEffect(marks, 3)); +console.log(marks.map((p) => p.w).join(" ")); + +// The failing statement is followed by `continue`, `break` and `return`. +function control(n: number, step: number): number { + let acc = 0; + let i = 0; + while (i < n) { + const v = i * step; + i++; + if (v * v > 400) break; + if ((i & 1) === 0) continue; + acc += v * 3; + if (acc > 1000) return -acc; + } + return acc; +} +for (const step of [1, 0.5, 2, 0.75, 7, 1.5, 3, 0.25, 5, 1.75, 4]) { + show(`control ${step}`, control(40, step)); +} + +// do-while: the check fails part-way through, on some entries only. +function steps(start: number, factor: number): number { + let v = start; + let k = 0; + do { + v = v * factor - k; + k++; + } while (k < 6); + return v; +} +for (let r = 0; r < 20; r++) show(`steps ${r}`, steps(r % 3 === 0 ? r + 0.5 : r, 3)); + +// Overflow past 2^53 on later entries only. +function powers(base: number, n: number): number { + let p = 1; + for (let i = 0; i < n; i++) p = p * base; + return p; +} +for (const n of [3, 10, 40, 80, 5, 60, 2, 70, 1, 90, 0, 4]) show(`powers ${n}`, powers(3, n)); diff --git a/tests/harness/corpus-costs/7321-speculative-loop-bail-sites.ts.json b/tests/harness/corpus-costs/7321-speculative-loop-bail-sites.ts.json new file mode 100644 index 0000000000..1e5609a660 --- /dev/null +++ b/tests/harness/corpus-costs/7321-speculative-loop-bail-sites.ts.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 2600 } From fbb4fbc5b0c59c91bbb6f2e3fc15f97c7fa203dc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:07:37 -0500 Subject: [PATCH 153/395] Encode posted records from their fields, and queue a message in one block postMessage of a typed record boxes it as a live record capsule, and the message encoder materialized the capsule's checked-dynamic view to read it: it built the view object, listed its own keys, tested each one for enumerability and read it back, then released the view and the key list. On an object-only run of bench/channel-messages that was about half of the encode time. - A live record capsule whose view was never created (no undeclared properties, no writes through an untyped view) now encodes from the record's field hooks (ScrDynTypedRecordOps), which answer exactly the view's keys, order and boxed values. Nested records encode the same way to a depth of 32, then through their views as before. Capsules with a view, published values, and every other value encode as before. - The mail event that carries a message lives in the message's block, so a queued message is one allocation instead of two. Corpus 7139 and 7360 post typed records on a MessageChannel and between a worker and the main thread: optional fields, nested records, a record reached twice, a chain deeper than the recursion limit, a parsed record with undeclared keys, and a function-valued field. --- .../corpus/7139-message-typed-records.ts.json | 6 ++ .../corpus/7360-worker-typed-records.ts.json | 6 ++ packages/runtime/src/scr_dyn.h | 3 + packages/runtime/src/scr_json.c | 4 ++ packages/runtime/src/scr_mailbox.c | 8 ++- packages/runtime/src/scr_mailbox.h | 5 ++ packages/runtime/src/scr_message.c | 53 +++++++++++++++++- packages/runtime/src/scr_worker.c | 5 +- tests/corpus/7139-message-typed-records.ts | 55 +++++++++++++++++++ tests/corpus/7360-worker-typed-records.ts | 37 +++++++++++++ .../7139-message-typed-records.ts.json | 1 + .../7360-worker-typed-records.ts.json | 1 + 12 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7139-message-typed-records.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7360-worker-typed-records.ts.json create mode 100644 tests/corpus/7139-message-typed-records.ts create mode 100644 tests/corpus/7360-worker-typed-records.ts create mode 100644 tests/harness/corpus-costs/7139-message-typed-records.ts.json create mode 100644 tests/harness/corpus-costs/7360-worker-typed-records.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7139-message-typed-records.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7139-message-typed-records.ts.json new file mode 100644 index 0000000000..5e50dcd138 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7139-message-typed-records.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7139-message-typed-records.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7360-worker-typed-records.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7360-worker-typed-records.ts.json new file mode 100644 index 0000000000..4925143ad3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7360-worker-typed-records.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7360-worker-typed-records.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 2602e0341f..62987708a8 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -364,6 +364,9 @@ typedef struct ScrDynTypedRecordOps { * for-in over a record with every field present (atomic slot). */ ScrArr *keys; } ScrDynTypedRecordOps; +/* A record capsule's ops while it answers from the native record (no view + * created yet), or NULL. */ +const ScrDynTypedRecordOps *scr_dyn_typed_ref_live_record(const ScrDyn *d); /* Prototype members of a compiled class seen as `unknown`, emitted per * class by the compiler: the string names of methods, getters and setters diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 355a34aee0..1c85a928ad 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -3591,6 +3591,10 @@ static const ScrDynTypedRecordOps *scr_typed_ref_record_ops(const ScrDyn *d) { return (const ScrDynTypedRecordOps *)(const void *)d->v.typed_ref.array; } +const ScrDynTypedRecordOps *scr_dyn_typed_ref_live_record(const ScrDyn *d) { + return d->kind == SCR_DYN_TYPED_REF ? scr_typed_ref_record_ops(d) : NULL; +} + static bool scr_typed_ref_record_field(const ScrDynTypedRecordOps *ops, const ScrStr *key, size_t *out) { /* Compiled reads pass the interned literal the field names also use. */ diff --git a/packages/runtime/src/scr_mailbox.c b/packages/runtime/src/scr_mailbox.c index a8dc7e5cc5..b2af227747 100644 --- a/packages/runtime/src/scr_mailbox.c +++ b/packages/runtime/src/scr_mailbox.c @@ -140,9 +140,11 @@ ScrMailbox *scr_mailbox_retain(ScrMailbox *mailbox) { void scr_mail_event_free(ScrMailEvent *event) { if (!event) return; - scr_message_free(event->message); free(event->data); - free(event); + ScrMessage *message = event->message; + /* An event in its message's block goes with the message. */ + if (!event->in_message) free(event); + scr_message_free(message); } void scr_mailbox_release(ScrMailbox *mailbox) { @@ -460,7 +462,7 @@ void scr_channel_set_owner(ScrChannel *channel, int side, ScrMailbox *owner, uns * messages as ready, so it does not wait while the queue is non-empty. */ bool scr_channel_post(ScrChannel *channel, int side, ScrMessage *message, unsigned message_kind, unsigned wake_kind) { - ScrMailEvent *event = calloc(1, sizeof(*event)); + ScrMailEvent *event = message ? scr_message_mail_event(message) : calloc(1, sizeof(*event)); if (!event) scr_trap("scriptc: out of memory\n"); event->kind = message_kind; event->message = message; diff --git a/packages/runtime/src/scr_mailbox.h b/packages/runtime/src/scr_mailbox.h index 4e5f253b56..a4acfccd07 100644 --- a/packages/runtime/src/scr_mailbox.h +++ b/packages/runtime/src/scr_mailbox.h @@ -12,8 +12,13 @@ typedef struct ScrMailEvent { unsigned char *data; /* raw bytes (a worker's captured stdout or stderr), owned */ size_t length; struct ScrMailEvent *next; + bool in_message; /* the event is the one message's block holds (scr_message_mail_event) */ } ScrMailEvent; +/* The event that carries `message`, in the message's own block: a queued + * message is then one allocation. Freeing the event frees the message. */ +ScrMailEvent *scr_message_mail_event(ScrMessage *message); + ScrMailbox *scr_mailbox_new(void); ScrMailbox *scr_mailbox_retain(ScrMailbox *mailbox); void scr_mailbox_release(ScrMailbox *mailbox); diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index 5082661035..29cbb1be0e 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -1,4 +1,5 @@ #include "scr_message.h" +#include "scr_mailbox.h" #include #include @@ -72,6 +73,7 @@ struct ScrMessage { bool storage; /* has STORAGE nodes */ bool releases; /* has STORAGE, PORT or HANDLE nodes, which own resources */ bool readable; /* see scr_message_readable */ + ScrMailEvent event; /* the event that carries the message (scr_message_mail_event) */ }; typedef struct { @@ -98,6 +100,10 @@ typedef struct { * and properties allocates only its block. */ #define SCR_MESSAGE_INLINE 8 +/* How deep live records nest before the rest of a graph encodes through + * the visit stack (scr_message_value recurses for them). */ +#define SCR_MESSAGE_RECORD_DEPTH 32 + typedef struct { ScrMessageNode *nodes; ScrMessageVisit *visits; @@ -121,6 +127,7 @@ typedef struct { bool storage; bool releases; bool shared; /* a node was reached twice */ + size_t record_depth; /* live records being encoded from their fields */ bool adopts; /* has PORT or HANDLE nodes */ ScrMessageNode inline_nodes[SCR_MESSAGE_INLINE]; ScrMessageVisit inline_visits[SCR_MESSAGE_INLINE]; @@ -148,6 +155,7 @@ static void scr_message_encoder_init(ScrMessageEncoder *encoder, const ScrDyn *t encoder->transfer = transfer; encoder->uncaught = uncaught; encoder->storage = encoder->releases = encoder->shared = encoder->adopts = false; + encoder->record_depth = 0; } static bool scr_message_listed(const ScrMessageEncoder *encoder, const ScrDyn *value) { @@ -321,6 +329,9 @@ static ScrDyn *scr_message_uncaught_keys(const ScrDyn *view) { return keys; } +static void scr_message_property(ScrMessageEncoder *encoder, size_t id, + const ScrStr *key, size_t value, bool hidden); + static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) { bool uncaught = encoder->uncaught; encoder->uncaught = false; @@ -382,6 +393,39 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) node->v.published = (ScrDyn *)input; return id; } + /* A live record capsule (scr_dyn_typed_ref_bind_record) whose view was + * never created encodes its fields from the record: the view would hold + * exactly those keys, in that order, with the values its ops box. Nested + * records encode the same way up to a depth, then through their views + * and the visit stack. */ + const ScrDynTypedRecordOps *record = + input->kind == SCR_DYN_TYPED_REF && encoder->record_depth < SCR_MESSAGE_RECORD_DEPTH + ? scr_dyn_typed_ref_live_record(input) : NULL; + if (record) { + encoder->visits[id].view = (ScrDyn *)input; + encoder->nodes[id].kind = SCR_MESSAGE_OBJECT; + void *rec = input->v.typed_ref.ptr; + encoder->record_depth++; + size_t result = id; + for (size_t i = 0; i < record->count; i++) { + if (!record->fixed && !record->has(rec, i)) continue; + ScrDyn *child = record->get(rec, i); + if (!child || scr_exc_pending()) { + scr_dyn_release(child); + result = SIZE_MAX; + break; + } + size_t next = scr_message_value(encoder, child); + scr_dyn_release(child); + if (next == SIZE_MAX) { + result = SIZE_MAX; + break; + } + scr_message_property(encoder, id, record->names[i], next, false); + } + encoder->record_depth--; + return result; + } ScrDyn *value = input->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(input) : (ScrDyn *)input; encoder->visits[id].view = value; @@ -756,8 +800,8 @@ static ScrMessage *scr_message_pack(ScrMessageEncoder *encoder) { ScrMessageProperty *slots = (ScrMessageProperty *)(nodes + count); ScrMessageTyped *records = (ScrMessageTyped *)(slots + properties); char *text = (char *)(records + typed); - *message = (ScrMessage){nodes, count, encoder->storage, encoder->releases, - !encoder->shared && !encoder->adopts}; + *message = (ScrMessage){.nodes = nodes, .count = count, .storage = encoder->storage, + .releases = encoder->releases, .readable = !encoder->shared && !encoder->adopts}; memcpy(nodes, encoder->nodes, count * sizeof(*nodes)); if (typed) memcpy(records, encoder->typed, typed * sizeof(*records)); size_t first = 0; @@ -1003,6 +1047,11 @@ done:; bool scr_message_readable(const ScrMessage *message) { return message->readable; } +ScrMailEvent *scr_message_mail_event(ScrMessage *message) { + message->event = (ScrMailEvent){.message = message, .in_message = true}; + return &message->event; +} + int32_t scr_message_kind(const ScrMessage *message, size_t node) { return message->nodes[node].kind; } double scr_message_number(const ScrMessage *message, size_t node) { return message->nodes[node].v.number; } diff --git a/packages/runtime/src/scr_worker.c b/packages/runtime/src/scr_worker.c index 67f4ec0846..59e245ea0f 100644 --- a/packages/runtime/src/scr_worker.c +++ b/packages/runtime/src/scr_worker.c @@ -99,9 +99,10 @@ static bool scr_worker_stats(void) { static ScrMailEvent *scr_worker_event(unsigned kind, uint64_t source, int code, ScrMessage *message) { - ScrMailEvent *event = calloc(1, sizeof(*event)); + ScrMailEvent *event = message ? scr_message_mail_event(message) : calloc(1, sizeof(*event)); if (!event) scr_trap("scriptc: out of memory\n"); - *event = (ScrMailEvent){.kind = kind, .source = source, .code = code, .message = message}; + *event = (ScrMailEvent){ + .kind = kind, .source = source, .code = code, .message = message, .in_message = message != NULL}; return event; } diff --git a/tests/corpus/7139-message-typed-records.ts b/tests/corpus/7139-message-typed-records.ts new file mode 100644 index 0000000000..678b9cc483 --- /dev/null +++ b/tests/corpus/7139-message-typed-records.ts @@ -0,0 +1,55 @@ +import { MessageChannel } from "node:worker_threads"; + +// Typed records posted on a MessageChannel arrive as plain objects with +// the same keys, order and values as in Node: optional fields present and +// absent, nested records, a record reached twice, a record nested deeper +// than the encoder recurses, a record parsed with undeclared keys, and a +// function-valued field, which postMessage rejects. +type Point = { x: number; y: number }; +type Item = { id: number; name: string; done: boolean; pos: Point; note?: string }; +type Pair = { left: Point; right: Point }; +type Link = { value: number; next?: Link }; + +const { port1, port2 } = new MessageChannel(); +const lines: string[] = []; +const expected = 7; +port1.on("message", (value: unknown) => { + let checks = ""; + if (!Array.isArray(value)) { + const object = value as Record; + if ("left" in object) checks += ` same ${object["left"] === object["right"]}`; + const pos = object["pos"] as Record | undefined; + if (pos !== undefined) checks += ` negative zero ${Object.is(pos["x"], -0)}`; + } + lines.push(JSON.stringify(value) + checks); + if (lines.length === expected) { + for (const line of lines) console.log(line); + port1.close(); + } +}); + +const item: Item = { id: 1, name: "first", done: false, pos: { x: 1, y: 2 } }; +port2.postMessage(item); +const noted: Item = { id: 2, name: "", done: true, pos: { x: -0, y: 0.5 }, note: "n" }; +port2.postMessage(noted); + +const shared: Point = { x: 3, y: 4 }; +const pair: Pair = { left: shared, right: shared }; +port2.postMessage(pair); + +let chain: Link = { value: 0 }; +for (let i = 1; i < 40; i++) chain = { value: i, next: chain }; +port2.postMessage(chain); + +const parsed = JSON.parse('{"y":6,"extra":true,"x":5}') as Point; +port2.postMessage(parsed); +port2.postMessage({ x: 7, y: 8 } as Point); +port2.postMessage([item.pos, noted.pos]); + +type WithFunction = { id: number; run: () => number }; +const bad: WithFunction = { id: 9, run: () => 1 }; +try { + port2.postMessage(bad); +} catch (error) { + console.log("rejected", (error as Error).name); +} diff --git a/tests/corpus/7360-worker-typed-records.ts b/tests/corpus/7360-worker-typed-records.ts new file mode 100644 index 0000000000..6749398f38 --- /dev/null +++ b/tests/corpus/7360-worker-typed-records.ts @@ -0,0 +1,37 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +// Typed records posted to a worker and back keep their keys, order and +// values, as in Node: records with optional fields, nested records and a +// record in workerData. +type Point = { x: number; y: number }; +type Job = { id: number; label: string; urgent: boolean; at: Point; note?: string }; +type Reply = { id: number; sum: number; label: string }; + +if (isMainThread) { + const start: Point = { x: 10, y: 20 }; + const worker = new Worker(new URL(import.meta.url), { workerData: start }); + const replies: string[] = []; + worker.on("message", (reply: Reply) => { + replies.push(`${JSON.stringify(reply)} ${Object.keys(reply).join(",")}`); + if (replies.length === 3) { + for (const line of replies) console.log(line); + void worker.terminate(); + } + }); + const jobs: Job[] = [ + { id: 1, label: "a", urgent: false, at: { x: 1, y: 2 } }, + { id: 2, label: "b", urgent: true, at: { x: 3, y: 4 }, note: "n" }, + { id: 3, label: "", urgent: false, at: { x: -1, y: 0.25 } }, + ]; + for (const job of jobs) worker.postMessage(job); +} else { + const origin = workerData as Point; + parentPort!.on("message", (job: Job) => { + const reply: Reply = { + id: job.id, + sum: job.at.x + job.at.y + origin.x + origin.y, + label: `${job.label}${job.urgent ? "!" : ""}${job.note ?? ""}`, + }; + parentPort!.postMessage(reply); + }); +} diff --git a/tests/harness/corpus-costs/7139-message-typed-records.ts.json b/tests/harness/corpus-costs/7139-message-typed-records.ts.json new file mode 100644 index 0000000000..f0ed6cbc2a --- /dev/null +++ b/tests/harness/corpus-costs/7139-message-typed-records.ts.json @@ -0,0 +1 @@ +{ "plain": 4300, "san": 4400 } diff --git a/tests/harness/corpus-costs/7360-worker-typed-records.ts.json b/tests/harness/corpus-costs/7360-worker-typed-records.ts.json new file mode 100644 index 0000000000..cf0403d1de --- /dev/null +++ b/tests/harness/corpus-costs/7360-worker-typed-records.ts.json @@ -0,0 +1 @@ +{ "plain": 2700, "san": 2900 } From 712b4e84a1e52dca58f559a20123f935e68e54c5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:12:12 -0500 Subject: [PATCH 154/395] 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 2a3f11be91..c5fce2423f 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 9a65d9a242..f18e4a7d52 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 d31f14d12b..6d1593a55f 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 ea2dd379d4..9096316a3b 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 fcdbe86947..6b7dc17414 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 0000000000..f8afbe00ba --- /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 ec685656dc..387885c42c 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 e899130d86..a5853ad59d 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 0000000000..e613c81f09 --- /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 0000000000..cf8ef4d26e --- /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 155/395] 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 6d22bf3bf6..354e631c6b 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 09e6551c30..3aa9a28a73 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 7239db7d89..0227776abd 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 ed17859126..ed1029b85f 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 0000000000..4ce307c5c3 --- /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 0000000000..1ddea603db --- /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 0000000000..e1f5a78373 --- /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 0000000000..6dcb4b57d5 --- /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 0000000000..9941ce5381 --- /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 0000000000..74df88f0c1 --- /dev/null +++ b/tests/harness/corpus-costs/7301-running-extrema-branches.ts.json @@ -0,0 +1 @@ +{ "plain": 5300, "san": 5800 } From d406f633bdb2d609e8c9a85a107ff275fe46bf28 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:18:18 -0500 Subject: [PATCH 156/395] Keep the receiver index within what the native frontend lowers The receiver index read its methods or members table by a computed key and iterated generic method names through `?.keys() ?? []`. The native frontend refuses dynamic keyed reads of records and cannot convert the iterator-or-array union, so the complete frontend no longer lowered. The table is now selected with a conditional and the generic method names are iterated under a check. --- .../frontend/lowering/class-dynamic-dispatch.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 79edae4334..79deb2aeb2 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -510,10 +510,11 @@ function receiverIndex(lowerer: Lowerer, plans: readonly PropertyReceiver[]): Re position, ); } - for (const name of c.genericMethods?.keys() ?? []) { - add(methods, name, position); - add(members, name, position); - } + if (c.genericMethods) + for (const name of c.genericMethods.keys()) { + add(methods, name, position); + add(members, name, position); + } } for (const name of info.fields.keys()) add(members, name, position); }); @@ -1430,9 +1431,8 @@ export class ClassDynamicDispatch { ): readonly PropertyReceiver[] => { if (name.startsWith("emit:")) return plans; const found = indexOf(plans); - return mergePositions(found[table].get(name) ?? [], found.rooted).map( - (position) => plans[position]!, - ); + const positions = (table === "methods" ? found.methods : found.members).get(name); + return mergePositions(positions ?? [], found.rooted).map((position) => plans[position]!); }; const candidates = (method: string): ClassInfo[] => { const found = byMethod.get(method); From 14694925c7d2edfa5c681c2d623f39b0ce3494af Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:32:24 -0500 Subject: [PATCH 157/395] 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 c74969d10d..f16d5ee5d0 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 8a29e7c5da..7c62aa05e0 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 6986330f53..007f8a7664 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 f11746eda3..cff612d29c 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 cfc8de8065..4b24406743 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 2f3e25b431fc13f0f8a08fdb0505b0c277dc734a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:35:56 -0500 Subject: [PATCH 158/395] Accept Buffer-compatible chunk annotations and keep super(undefined) in byte mode --- .../src/frontend/lowering/lower-stream.ts | 18 ++++++---- tests/corpus/7240-objectmode-streams.ts | 33 ++++++++++++++++- .../stream-byte-mode-chunk-fences.ts | 36 +++++++++++++++++++ .../stream-byte-mode-chunk-fences.ts.txt | 8 +++++ 4 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 tests/diagnostics/stream-byte-mode-chunk-fences.ts create mode 100644 tests/harness/__snapshots__/stream-byte-mode-chunk-fences.ts.txt diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index cb81348eb5..6540b3a99a 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -312,11 +312,13 @@ function lowerStreamCallback( // A write or transform chunk of another type converts from the // chunk's value when the callback runs (an object-mode side's value), // or throws a TypeError there — on a side known to be byte mode the - // chunk is a Buffer, so the declared type is the fence's; an encoding - // of another type (`string | null | undefined`) converts from the - // encoding argument's value. + // chunk is a Buffer, so the declared type must be Node's or a + // supertype of it (`Buffer | string`); an encoding of another type + // (`string | null | undefined`) converts from the encoding argument's + // value. if ( - ((i === 0 && chunkValues) || i === 1) && + ((i === 0 && (chunkValues || widensInto(lowerer, fullTuple[0]!, funcType.params[0]!, loc))) || + i === 1) && (which === "write" || which === "transform") && funcType.params[i]!.kind !== "func" && canConvertToDyn( @@ -1369,11 +1371,13 @@ export function lowerStreamSuperCall( // set objectMode, and a literal says so with objectMode (or a Duplex's // writableObjectMode) true; otherwise _write and _transform receive // Buffers. + // super(undefined) and super(null) pass no options (followOptionsArg + // answers null), so their sides are byte mode too. const optsNode = argNodes[0] !== undefined ? followOptionsArg(lowerer, argNodes[0]) : null; const chunkValues = - argNodes[0] !== undefined && - (optsNode === null || - !ts.isObjectLiteralExpression(optsNode) || + optsNode !== null && + optsNode !== undefined && + (!ts.isObjectLiteralExpression(optsNode) || literalWritesValues(optsNode, cls !== "%Readable" && cls !== "%Writable")); const fallback = (name: string): IrExpr | null => { const methodName = UNDERSCORE_METHODS.get(name); diff --git a/tests/corpus/7240-objectmode-streams.ts b/tests/corpus/7240-objectmode-streams.ts index 9316ff4802..47f799eeed 100644 --- a/tests/corpus/7240-objectmode-streams.ts +++ b/tests/corpus/7240-objectmode-streams.ts @@ -2,7 +2,8 @@ // and Writable in pipeline(), typed 'data' listeners, a Transform with // readableObjectMode parsing bytes into records, an object-mode Duplex, // a PassThrough's object-counted high-water mark, and Readable.from over -// a Map, a Set of records and an array mixing strings and Buffers. +// a Map, a Set of records and an array mixing strings and Buffers, and +// byte-mode chunk parameters typed Buffer | string. import { Duplex, PassThrough, Readable, Transform, Writable } from "node:stream"; import { pipeline } from "node:stream/promises"; @@ -119,6 +120,34 @@ async function collections(): Promise { for await (const t of Readable.from(tags)) console.log("set", JSON.stringify(t)); } +// Byte-mode sides pass Buffers: a chunk parameter typed as a supertype +// of Buffer takes them, inline and as an underscore method, and +// super(undefined) leaves the stream in byte mode. +class Collector extends Writable { + constructor() { + super(undefined); + } + _write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log("collector", Buffer.isBuffer(chunk), typeof chunk === "string" ? chunk : chunk.toString()); + cb(); + } +} + +async function byteChunks(): Promise { + const inline = new Writable({ + write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log("inline", Buffer.isBuffer(chunk), typeof chunk === "string" ? chunk : chunk.toString()); + cb(); + }, + }); + inline.write("one"); + inline.end(Buffer.from("two")); + await new Promise((resolve) => inline.on("finish", () => resolve())); + const collector = new Collector(); + collector.end("three"); + await new Promise((resolve) => collector.on("finish", () => resolve())); +} + async function main(): Promise { await records(); await collections(); @@ -126,5 +155,7 @@ async function main(): Promise { sided(); duplex(); passthrough(); + await new Promise((resolve) => setTimeout(resolve, 5)); + await byteChunks(); } main(); diff --git a/tests/diagnostics/stream-byte-mode-chunk-fences.ts b/tests/diagnostics/stream-byte-mode-chunk-fences.ts new file mode 100644 index 0000000000..6afd1d7946 --- /dev/null +++ b/tests/diagnostics/stream-byte-mode-chunk-fences.ts @@ -0,0 +1,36 @@ +// A byte-mode stream's write chunks are Buffers: an underscore method +// whose chunk parameter has another type is fenced, and super(undefined) +// passes no options, so the stream is in byte mode. A parameter that +// admits the Buffer (Buffer | string) is not fenced, inline or as a +// method. +import { Writable } from "node:stream"; + +class Numbers extends Writable { + constructor() { + super(undefined); + } + _write(chunk: number, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log(chunk); + cb(); + } +} +new Numbers().end("x"); + +const accepted = new Writable({ + write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log(typeof chunk === "string" ? chunk : chunk.toString()); + cb(); + }, +}); +accepted.end("z"); + +class Wide extends Writable { + constructor() { + super(undefined); + } + _write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void { + console.log(typeof chunk === "string" ? chunk : chunk.toString()); + cb(); + } +} +new Wide().end("w"); diff --git a/tests/harness/__snapshots__/stream-byte-mode-chunk-fences.ts.txt b/tests/harness/__snapshots__/stream-byte-mode-chunk-fences.ts.txt new file mode 100644 index 0000000000..39a06dfd7f --- /dev/null +++ b/tests/harness/__snapshots__/stream-byte-mode-chunk-fences.ts.txt @@ -0,0 +1,8 @@ +stream-byte-mode-chunk-fences.ts:10:5 - error SC2020: 'a '_write' method whose parameter 0 is 'number' where Node passes 'Uint8Array'' is part of the standard library types but has no scriptc lowering yet + + 9 | constructor() { + 10 | super(undefined); + | ^~~~~~~~~~~~~~~~ + 11 | } + + hint: annotate the parameter with the Node signature's type or a supertype of it (chunks are Buffers here) \ No newline at end of file From 01939bc0721d00fe519fc23a66026a7f96193ab8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:41:08 -0500 Subject: [PATCH 159/395] 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 822ee29e25..ac4e287087 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 1b84cf0a03..6a0123559a 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 4488f90cbc..4d8d5fecf6 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 6911ef98b8..86ea79907c 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 3d9d410720..bf89784b73 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 7239db7d89..5376d4f535 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 41ecd189e4..726ec20c5f 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 612c47ea04..cc22594d52 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 b4e285ee98..a7ce0a1064 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 180f404351..2f839a20b7 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 4a395feb2c..8008008f9a 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 c00d1b4f66..c63bc4cc93 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 6c41b3dbd8..697912934e 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 7a6a76cf9f..7fec7bd07a 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 df4da1738f..125685dc62 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 5e5029e149..5956d5dd2a 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 a333aab95c..25a074f499 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 f6d98654af..c85370add7 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 4be596ab5b..3f7dcac459 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 99caa31a8f..1d0cf64b65 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 675a13ca98..3cb6c5b55f 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 b62cba896c..158697a143 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 dbd74f3acc..25bbcf7aeb 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 58272cf675..f94554642c 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 d943acfb22..6e4ead89ac 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 160/395] 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 831fec83d2..67706a285d 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 2f6afe734a..41026a7c84 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 6e75e59f753e9dd25ba5b915220f036d2421bc10 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:52:18 -0500 Subject: [PATCH 161/395] Check each speculative site of a loop in the third-copy emission test Since dense-array loops bounded by an invariant limit are versioned, the test's loop has a speculative site in each dense copy. The test paired every resume point with the first site's labels; it now checks each site's resume points against that site's third and fast copies. --- .../test/speculative-loop-emission.test.ts | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/compiler/test/speculative-loop-emission.test.ts b/packages/compiler/test/speculative-loop-emission.test.ts index 78a7de295f..a691d856d5 100644 --- a/packages/compiler/test/speculative-loop-emission.test.ts +++ b/packages/compiler/test/speculative-loop-emission.test.ts @@ -47,24 +47,33 @@ test("a failed check finishes its iteration outside the ordinary copy", async () const llvm = await emitLlvm(source); const fn = /^define internal double @sc_bf_scaled\([^]*?^}/m.exec(llvm)?.[0] ?? ""; const order = labels(fn); - const slow = order.findIndex((l) => l.startsWith("spec.slow")); - const tail = order.findIndex((l) => l.startsWith("spec.tail")); - const fast = order.findIndex((l) => l.startsWith("spec.fast")); - expect(slow).toBeGreaterThanOrEqual(0); - expect(tail).toBeGreaterThan(slow); - expect(fast).toBeGreaterThan(tail); - // Every resume point is in the third copy, between its start and the - // fast copy, so the ordinary copy has one entry. - const resumes = order.flatMap((l, i) => (l.startsWith("spec.resume") ? [i] : [])); - expect(resumes.length).toBeGreaterThan(0); - for (const i of resumes) { - expect(i).toBeGreaterThan(tail); - expect(i).toBeLessThan(fast); + // The loop may have several speculative sites (one in each copy of a + // dense-array loop). Each site emits its ordinary copy, its third copy + // and its fast copy in that order. + const slows = order.flatMap((l, i) => (l.startsWith("spec.slow") ? [i] : [])); + expect(slows.length).toBeGreaterThan(0); + let resumes = 0; + for (const slow of slows) { + const next = (prefix: string): number => + order.findIndex((l, i) => i > slow && l.startsWith(prefix)); + const tail = next("spec.tail"); + const fast = next("spec.fast"); + expect(tail).toBeGreaterThan(slow); + expect(fast).toBeGreaterThan(tail); + // Every resume point of the site is in its third copy, between that + // copy's start and the fast copy, so the ordinary copy has one entry. + for (let i = slow + 1; i < fast; i++) { + if (!order[i]!.startsWith("spec.resume")) continue; + expect(i).toBeGreaterThan(tail); + resumes++; + } + // The third copy's header leaves for the ordinary copy's header. + const header = order[slow + 1]!; + const third = fn.slice(fn.indexOf(`\n${order[tail]}:`), fn.indexOf(`\n${order[fast]}:`)); + expect(third).toContain(`br label %${header}`); } - // The third copy's header leaves for the ordinary copy's header. - const header = order[slow + 1]!; - const third = fn.slice(fn.indexOf(`\n${order[tail]}:`), fn.indexOf(`\n${order[fast]}:`)); - expect(third).toContain(`br label %${header}`); + expect(resumes).toBe(order.filter((l) => l.startsWith("spec.resume")).length); + expect(resumes).toBeGreaterThan(0); }); test("a loop site stops entering its fast copy after repeated bails", async () => { From 8aac437ed281fbedde2f96296c6ca867035b3e0a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:53:50 -0500 Subject: [PATCH 162/395] 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 9db04e95b5..d956409781 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 7f3910c158..44d38acd46 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 b02a0a843c..a42cadb48e 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 0000000000..b93eb61b67 --- /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 0000000000..2514bcaa81 --- /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 0000000000..bd2c8e4d9c --- /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 0000000000..ae5a8d847b --- /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 0000000000..c480bea3f9 --- /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 0000000000..b86a9e70a9 --- /dev/null +++ b/tests/harness/corpus-costs/7185-array-callback-arguments-length.ts.json @@ -0,0 +1 @@ +{ "plain": 2528, "san": 3201 } From 142e10a0c428065a37eac0228b3a2b83fef5694f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:55:08 -0500 Subject: [PATCH 163/395] Run counters that step beside a counted loop's counter as integers `for (let i = 0, p = 0; i < count; i++, p += 4)` kept both counters as doubles: the counted-loop match takes a single declaration and a single update. A counted loop now also accepts further counters declared in its head and stepped by a constant in its update, when the condition does not read them and the body does not write them. On body entry k such a counter holds `start + step * k`, so its range follows from the driver's: k is at most the driver's span over its step. When that range can pass 2^53, the versioned loop tests `|start| + |step| * span / |driver step| <= 2^52` at run time, and an unproven start is tested for an exact safe integer, as the driver's is. Nested update blocks (`i++, j += 2, k--`) are flattened. Byte windows and dense loops still take only single-counter loops. --- .../src/backend/llvm/counted-loops.ts | 45 ++++- .../backend/llvm/dense-array-loops.test.ts | 1 + .../src/backend/llvm/emit-statements.ts | 36 +++- .../compiler/src/ir/integer-loops.test.ts | 100 ++++++++++ packages/compiler/src/ir/integer-loops.ts | 188 +++++++++++++----- .../7141-comoving-loop-counters.ts.json | 6 + tests/corpus/7141-comoving-loop-counters.ts | 144 ++++++++++++++ .../7141-comoving-loop-counters.ts.json | 1 + 8 files changed, 467 insertions(+), 54 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7141-comoving-loop-counters.ts.json create mode 100644 tests/corpus/7141-comoving-loop-counters.ts create mode 100644 tests/harness/corpus-costs/7141-comoving-loop-counters.ts.json diff --git a/packages/compiler/src/backend/llvm/counted-loops.ts b/packages/compiler/src/backend/llvm/counted-loops.ts index 02943b8523..ca2dd6b924 100644 --- a/packages/compiler/src/backend/llvm/counted-loops.ts +++ b/packages/compiler/src/backend/llvm/counted-loops.ts @@ -1,4 +1,9 @@ -import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; +import type { + IntegerCountedForLoop, + IntegerLoopFollower, + IntegerPairedForLoop, +} from "../../ir/integer-loops.js"; +import { FOLLOWER_GUARD_LIMIT } from "../../ir/integer-loops.js"; import type { IrExpr } from "../../ir/ir.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { f64Lit } from "./common.js"; @@ -61,9 +66,47 @@ export function emitCountedLoopSafety( ); } if (loop.startGuarded) safe = andSafe(host, safe, emitExactStart(host, loop.start)); + for (const follower of loop.followers) { + if (follower.startGuarded) safe = andSafe(host, safe, emitExactStart(host, follower.start)); + if (follower.tripGuarded) safe = andSafe(host, safe, emitFollowerReach(host, loop, follower)); + } return safe; } +/** `|start| + |step| * span / |driver step| <= 2^52` for a follower, where + * `span` is the distance from the driver's start to the limit. The body runs + * at most `span / |driver step| + 1` times, so every value the follower holds + * on a body entry is within the bound. NaN and infinities fail the ordered + * test. */ +function emitFollowerReach( + host: LlvmEmitterContext, + loop: IntegerCountedForLoop, + follower: IntegerLoopFollower, +): string { + const B = host.B; + const start = host.emitExpr(loop.start), + limit = host.emitExpr(loop.limit), + from = host.emitExpr(follower.start); + const span = B.tmp(), + reach = B.tmp(), + magnitude = B.tmp(), + total = B.tmp(), + ok = B.tmp(); + B.line( + loop.step > 0 + ? `${span} = fsub double ${limit.name}, ${start.name}` + : `${span} = fsub double ${start.name}, ${limit.name}`, + ); + B.line( + `${reach} = fmul double ${span}, ${f64Lit(Math.abs(follower.step) / Math.abs(loop.step))}`, + ); + host.declare(`declare double @llvm.fabs.f64(double)`); + B.line(`${magnitude} = call double @llvm.fabs.f64(double ${from.name})`); + B.line(`${total} = fadd double ${magnitude}, ${reach}`); + B.line(`${ok} = fcmp ole double ${total}, ${f64Lit(FOLLOWER_GUARD_LIMIT)}`); + return ok; +} + /** The run-time test of a guarded paired loop: each unproven start is an * exact safe integer. */ export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPairedForLoop): string { diff --git a/packages/compiler/src/backend/llvm/dense-array-loops.test.ts b/packages/compiler/src/backend/llvm/dense-array-loops.test.ts index 32c9cccab0..f7a4bc892b 100644 --- a/packages/compiler/src/backend/llvm/dense-array-loops.test.ts +++ b/packages/compiler/src/backend/llvm/dense-array-loops.test.ts @@ -120,6 +120,7 @@ const counted = ( limitGuarded: false, startGuarded: false, guardLimit: 0, + followers: [], ...fields, }); diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index 7239db7d89..c493f6c9c1 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -879,8 +879,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { !integerLoop && !countedLoop && emitter.countedLoopsEnabled ? matchIntegerPairedForLoop(s, emitter.numericLocals, emitter.integerRanges) : null; + // Byte windows and dense loops reason about one counter. const window = - countedLoop && emitter.byteWindowsEnabled + countedLoop && countedLoop.followers.length === 0 && emitter.byteWindowsEnabled ? matchByteWindow( s, countedLoop, @@ -898,7 +899,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { length: integerArrayLoop.limitReceiver as IrExpr & { kind: "varRef" }, range: { min: 0, max: 4294967294 }, } - : countedLoop && !countedLoop.guarded + : countedLoop && !countedLoop.guarded && countedLoop.followers.length === 0 ? countedDenseShape(s, countedLoop) : null; const dense = @@ -1030,6 +1031,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { let integerSlot: string | null = null; let countedLimit: string | null = null; const pairedSlots: string[] = []; + const followerSlots: string[] = []; if (integerLoop) { integerSlot = B.slot(); B.entryAllocas.push( @@ -1073,6 +1075,23 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { ? emitter.denseLoop.range : countedLoop.range, }); + for (const follower of countedLoop.followers) { + const slot = B.slot(); + B.entryAllocas.push( + `${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(follower.localId)!.name}`, + ); + const value = emitter.emitExpr(follower.start); + const followerStart = B.tmp(); + B.line(`${followerStart} = fptosi double ${value.name} to i64`); + B.line(`store i64 ${followerStart}, ptr ${slot}`); + followerSlots.push(slot); + emitter.integerLoopBindings.set(follower.localId, { + slot, + type: "i64", + signed: true, + range: follower.range, + }); + } countedLimit = emitCountedLoopLimit(emitter, countedLoop); } else if (pairedLoop) { for (const counter of [pairedLoop.low, pairedLoop.high]) { @@ -1212,6 +1231,13 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.line(`${old} = load i64, ptr ${integerSlot}`); B.line(`${next} = add nsw i64 ${old}, ${countedLoop.step}`); B.line(`store i64 ${next}, ptr ${integerSlot}`); + countedLoop.followers.forEach((follower, i) => { + const was = B.tmp(); + const now = B.tmp(); + B.line(`${was} = load i64, ptr ${followerSlots[i]}`); + B.line(`${now} = add nsw i64 ${was}, ${follower.step}`); + B.line(`store i64 ${now}, ptr ${followerSlots[i]}`); + }); } else if (pairedLoop) { [pairedLoop.low, pairedLoop.high].forEach((counter, i) => { const old = B.tmp(); @@ -1227,7 +1253,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.startBlock(le); emitter.releaseScope(emitter.scopes.pop()!); if (integerLoop) emitter.integerLoopBindings.delete(integerLoop.localId); - if (countedLoop) emitter.integerLoopBindings.delete(countedLoop.localId); + if (countedLoop) { + emitter.integerLoopBindings.delete(countedLoop.localId); + for (const follower of countedLoop.followers) + emitter.integerLoopBindings.delete(follower.localId); + } if (pairedLoop) { emitter.integerLoopBindings.delete(pairedLoop.low.localId); emitter.integerLoopBindings.delete(pairedLoop.high.localId); diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index 24bbf5be0f..e1453abadd 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -426,3 +426,103 @@ test("paired counters closing in on each other run as integers", () => { unused.body = []; expect(matchIntegerPairedForLoop(unused, vars)).toBeNull(); }); + +test("counters stepped beside the tested one run as integers", () => { + const vars = new Map(locals); + for (const id of ["j", "k", "limit", "from"]) + vars.set(id, { id, name: id, type: F64, mutable: true }); + vars.set("s", { id: "s", name: "s", type: STRING, mutable: false }); + const step = (id: string, op: "+" | "-", value = 1): IrStmt => ({ + kind: "assign", + localId: id, + value: { kind: "bin", op, left: ref(id), right: number(value), type: F64, loc }, + loc, + }); + const read = (index: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { + kind: "strIntrinsic", + method: "charCodeAt", + receiver: { kind: "varRef", localId: "s", type: STRING, loc }, + args: [index], + type: F64, + loc, + }, + loc, + }); + const comoving = ( + limit: IrExpr, + followerStart: IrExpr = number(0), + update: IrStmt = { kind: "block", body: [step("i", "+"), step("j", "+", 4)], loc }, + ) => + ({ + kind: "for", + loc, + init: { + kind: "block", + body: [ + { kind: "varDecl", localId: "i", init: number(0), loc }, + { kind: "varDecl", localId: "j", init: followerStart, loc }, + ], + loc, + }, + cond: { kind: "bin", op: "<", left: ref("i"), right: limit, type: BOOL, loc }, + update, + body: [read(ref("j"))], + }) as IrStmt & { kind: "for" }; + // A literal limit bounds the follower: at most 9 updates of 4 before the + // last body entry. + expect(matchIntegerCountedForLoop(comoving(number(10)), vars)).toMatchObject({ + localId: "i", + guarded: false, + range: { min: 0, max: 9 }, + followers: [ + { + localId: "j", + step: 4, + startGuarded: false, + tripGuarded: false, + range: { min: 0, max: 36 }, + }, + ], + }); + // An unknown limit or start is tested at run time. + expect(matchIntegerCountedForLoop(comoving(ref("limit")), vars)).toMatchObject({ + guarded: true, + followers: [{ tripGuarded: true, range: { min: 0, max: 2 ** 52 + 2 } }], + }); + expect(matchIntegerCountedForLoop(comoving(number(10), ref("from")), vars)).toMatchObject({ + guarded: true, + followers: [{ startGuarded: true, tripGuarded: true }], + }); + // A literal limit with known starts that can pass 2^53 is refused. + expect(matchIntegerCountedForLoop(comoving(number(10), number(2 ** 53 - 9)), vars)).toBeNull(); + // Nested update blocks (`i++, j += 4, k--`) step three counters. + const three = comoving(number(10), number(0), { + kind: "block", + body: [{ kind: "block", body: [step("i", "+"), step("j", "+", 4)], loc }, step("k", "-")], + loc, + }); + if (three.init?.kind === "block") + three.init.body.push({ kind: "varDecl", localId: "k", init: number(5), loc }); + expect(matchIntegerCountedForLoop(three, vars)?.followers).toMatchObject([ + { localId: "j", range: { min: 0, max: 36 } }, + { localId: "k", step: -1, range: { min: -4, max: 5 } }, + ]); + // A follower without a step, one the body writes, one the limit reads, a + // captured one, and a start that reads another counter are refused. + expect( + matchIntegerCountedForLoop( + comoving(number(10), number(0), { kind: "block", body: [step("i", "+")], loc }), + vars, + ), + ).toBeNull(); + const writing = comoving(number(10)); + writing.body.push(step("j", "+")); + expect(matchIntegerCountedForLoop(writing, vars)).toBeNull(); + expect(matchIntegerCountedForLoop(comoving(ref("j")), vars)).toBeNull(); + const boxed = new Map(vars); + boxed.set("j", { ...vars.get("j")!, boxed: true }); + expect(matchIntegerCountedForLoop(comoving(number(10)), boxed)).toBeNull(); + expect(matchIntegerCountedForLoop(comoving(number(10), ref("i")), vars)).toBeNull(); +}); diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index e80680d6e1..c4cdc327cd 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -28,8 +28,36 @@ export interface IntegerCountedForLoop { startGuarded: boolean; guardLimit: number; range: IntegerRange; + /** Counters declared and stepped beside this one that the condition does + * not test, such as `j` in `for (let i = 0, j = 0; i < n; i++, j += 4)`. */ + followers: IntegerLoopFollower[]; } +/** A counter that steps by a constant on every update of a counted loop. + * After `k` updates it holds `start + step * k`. */ +export interface IntegerLoopFollower { + localId: string; + start: IrExpr; + step: number; + /** As the driver's `startGuarded` (part of the loop's `guarded`). */ + startGuarded: boolean; + /** The follower's range depends on the run-time trip count: the versioned + * loop also tests `|start| + |step| * span / |driver step|` against + * `FOLLOWER_GUARD_LIMIT`, where `span` is the distance from the driver's + * start to the limit (part of the loop's `guarded`). */ + tripGuarded: boolean; + /** The follower's values on every body entry. */ + range: IntegerRange; +} + +/** The bound of the run-time follower test. */ +export const FOLLOWER_GUARD_LIMIT = 2 ** 52; +/** The magnitude a follower that passed the test can reach. The test's four + * f64 operations (the step ratio, the span, its product and the sum) each + * round down by at most a factor of 1 - 2^-53, so the exact sum is below + * `2^52 / (1 - 2^-53)^4`, which is less than 2^52 + 3. */ +const FOLLOWER_GUARD_RANGE = FOLLOWER_GUARD_LIMIT + 2; + function constantStep(update: IrStmt | null, id: string): number | null { if (update?.kind === "exprStmt" && update.expr.kind === "incDec" && update.expr.localId === id) return update.expr.op === "+" ? 1 : -1; @@ -115,33 +143,61 @@ export function matchIntegerCountedForLoop( ranges: IntegerRanges = new Map(), unaliased: ReadonlySet | null = null, ): IntegerCountedForLoop | null { - const init = stmt.init; - if (init?.kind !== "varDecl" || !init.init) return null; - const local = locals.get(init.localId); - if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null; - const start = init.init; - let startRange = - start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0) - ? { min: start.value, max: start.value } - : ranges.get(start); - // An unproven start is tested at run time, on a versioned loop. - const startGuarded = !startRange && start.kind !== "numLit" && start.type.kind === "f64"; - if (startGuarded) startRange = { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER }; - if ( - !startRange || - startRange.min < -Number.MAX_SAFE_INTEGER || - startRange.max > Number.MAX_SAFE_INTEGER || - !stableLoopValue(start, locals, stmt.body, false, local.id) - ) - return null; - const step = constantStep(stmt.update, local.id); - if (step === null || writesLocal(stmt.body, local.id)) return null; const cond = stmt.cond; if (cond?.kind !== "bin") return null; + const decls = stmt.init?.kind === "block" ? stmt.init.body : stmt.init ? [stmt.init] : []; + // `i++, j += 2, k--` lowers to nested blocks of steps. + const flat = (u: IrStmt | null): (IrStmt | null)[] => + u?.kind === "block" ? u.body.flatMap(flat) : [u]; + const updates = flat(stmt.update); + if (decls.length === 0 || updates.length !== decls.length) return null; + const declared = (e: IrExpr): string | null => + e.kind === "varRef" && decls.some((d) => d.kind === "varDecl" && d.localId === e.localId) + ? e.localId + : null; + // The condition tests the driving counter; every other declaration + // follows it. + const driver = declared(cond.left) ?? declared(cond.right); + const counterIds = decls.map((d) => (d.kind === "varDecl" ? d.localId : "")); + const stepOf = (id: string): number | null => { + const steps = updates.map((u) => constantStep(u, id)).filter((v) => v !== null); + return steps.length === 1 ? steps[0]! : null; + }; + // A counter's start is a stable integer that refers to no counter. An + // unproven start is tested at run time, on a versioned loop. + const counterStart = ( + id: string, + ): { start: IrExpr; range: IntegerRange; guarded: boolean; step: number } | null => { + const decl = decls.find((d) => d.kind === "varDecl" && d.localId === id); + if (decl?.kind !== "varDecl" || !decl.init) return null; + const local = locals.get(id); + if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null; + const start = decl.init; + const known = + start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0) + ? { min: start.value, max: start.value } + : ranges.get(start); + const guarded = !known && start.kind !== "numLit" && start.type.kind === "f64"; + const range = guarded ? { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER } : known; + const step = stepOf(id); + if ( + !range || + range.min < -Number.MAX_SAFE_INTEGER || + range.max > Number.MAX_SAFE_INTEGER || + !counterIds.every((counter) => stableLoopValue(start, locals, stmt.body, false, counter)) || + step === null || + writesLocal(stmt.body, id) + ) + return null; + return { start, range, guarded, step }; + }; + const head = driver === null ? null : counterStart(driver); + if (!head) return null; + const local = locals.get(driver!)!; + const { start, step, guarded: startGuarded, range: startRange } = head; let limit = cond.right, op: string = cond.op; if (cond.left.kind !== "varRef" || cond.left.localId !== local.id) { - if (cond.right.kind !== "varRef" || cond.right.localId !== local.id) return null; limit = cond.left; op = ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as Record)[op] ?? ""; } @@ -185,11 +241,68 @@ export function matchIntegerCountedForLoop( const limitGuarded = !boundRange || (step > 0 ? !(boundRange.max <= guardLimit) : !(boundRange.min >= guardLimit)); if (limitGuarded && limit.kind === "numLit") return null; - const guarded = limitGuarded || startGuarded; + const range = + step > 0 + ? { + min: startRange.min, + max: Math.max( + startRange.min, + Math.min( + Number.MAX_SAFE_INTEGER, + boundRange + ? Math.ceil(boundRange.max) - (inclusive ? 0 : 1) + : Number.MAX_SAFE_INTEGER, + ), + ), + } + : { + min: Math.min( + startRange.max, + Math.max( + -Number.MAX_SAFE_INTEGER, + boundRange + ? Math.floor(boundRange.min) + (inclusive ? 0 : 1) + : -Number.MAX_SAFE_INTEGER, + ), + ), + max: startRange.max, + }; + // On body entry k the driver holds `start + step * k`, so k is at most the + // driver's span over |step|, and a follower holds its start plus its own + // step times k. + const trips = Math.floor( + (step > 0 ? range.max - startRange.min : startRange.max - range.min) / Math.abs(step), + ); + const followers: IntegerLoopFollower[] = []; + for (const id of counterIds) { + if (id === local.id) continue; + const follower = counterStart(id); + // The limit is evaluated once; a follower changes on every update. + if (!follower || !stableLoopValue(limit, locals, stmt.body, false, id)) return null; + const reach = follower.step * trips; + const min = follower.range.min + Math.min(0, reach), + max = follower.range.max + Math.max(0, reach); + const tripGuarded = min < -Number.MAX_SAFE_INTEGER || max > Number.MAX_SAFE_INTEGER; + // Known starts and a literal limit are not tested at run time. + if (tripGuarded && !follower.guarded && !startGuarded && limit.kind === "numLit") return null; + followers.push({ + localId: id, + start: follower.start, + step: follower.step, + startGuarded: follower.guarded, + tripGuarded, + range: tripGuarded + ? { min: Math.max(-FOLLOWER_GUARD_RANGE, min), max: Math.min(FOLLOWER_GUARD_RANGE, max) } + : { min, max }, + }); + } + const guarded = + limitGuarded || startGuarded || followers.some((f) => f.startGuarded || f.tripGuarded); let useful = false; let cost = 0; const counter = (e: IrExpr): boolean => - (e.kind === "varRef" && e.localId === local.id) || + (e.kind === "varRef" && + (e.localId === local.id || followers.some((f) => f.localId === e.localId))) || (e.kind === "bin" && (e.op === "+" || e.op === "-" || e.op === "*") && (counter(e.left) || counter(e.right))); @@ -241,32 +354,6 @@ export function matchIntegerCountedForLoop( return (!guarded || ++cost <= 80) && everyStmtChild(s, expr, bodyStmt); }; if (!stmt.body.every(bodyStmt) || !useful) return null; - const range = - step > 0 - ? { - min: startRange.min, - max: Math.max( - startRange.min, - Math.min( - Number.MAX_SAFE_INTEGER, - boundRange - ? Math.ceil(boundRange.max) - (inclusive ? 0 : 1) - : Number.MAX_SAFE_INTEGER, - ), - ), - } - : { - min: Math.min( - startRange.max, - Math.max( - -Number.MAX_SAFE_INTEGER, - boundRange - ? Math.floor(boundRange.min) + (inclusive ? 0 : 1) - : -Number.MAX_SAFE_INTEGER, - ), - ), - max: startRange.max, - }; return { localId: local.id, start, @@ -278,6 +365,7 @@ export function matchIntegerCountedForLoop( startGuarded, guardLimit, range, + followers, }; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7141-comoving-loop-counters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7141-comoving-loop-counters.ts.json new file mode 100644 index 0000000000..392c9f10d7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7141-comoving-loop-counters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7141-comoving-loop-counters.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7141-comoving-loop-counters.ts b/tests/corpus/7141-comoving-loop-counters.ts new file mode 100644 index 0000000000..a756b7a466 --- /dev/null +++ b/tests/corpus/7141-comoving-loop-counters.ts @@ -0,0 +1,144 @@ +// For loops with a second counter that steps beside the tested one +// (`i++, j += 4`): pixel strides over typed arrays, interleaving into number +// arrays, descending and inclusive loops, three counters, early exits, +// starts and limits that are fractions, negative zero, NaN, infinite or +// near 2^53, counters that live across a yield or an await, and counters a +// closure captures, the body writes or the condition reads. +function luminance(px: Uint8Array, count: number): number[] { + const out: number[] = []; + for (let i = 0, p = 0; i < count; i++, p += 4) { + out.push((px[p]! * 77 + px[p + 1]! * 150 + px[p + 2]! * 29) >> 8); + } + return out; +} +const px = new Uint8Array(32); +for (let k = 0; k < px.length; k++) px[k] = (k * 37 + 11) & 255; +console.log(luminance(px, 8).join(",")); +// Past the end, reads are undefined and the sums NaN, which `>> 8` makes 0. +console.log(luminance(px, 10).join(",")); +console.log(luminance(px, 0).length, luminance(px, -3).length, luminance(px, 2.5).join(",")); +console.log(luminance(px, NaN).length); + +function interleave(a: number[], b: number[]): number[] { + const out: number[] = new Array(a.length * 2).fill(-1); + const n = a.length; + for (let i = 0, j = 0; i < n; i++, j += 2) { + out[j] = a[i]!; + out[j + 1] = b[i]!; + } + return out; +} +console.log(interleave([1, 2, 3, 4], [10, 20, 30, 40]).join(",")); +console.log(interleave([], []).length); + +// The second counter's start is not proven: charCodeAt truncates its index, +// and the counter's own values keep JS semantics. +function codes(s: string, count: number, from: number, step: number): string { + const out: (number | string)[] = []; + if (step === 3) { + for (let i = 0, j = from; i < count; i++, j += 3) out.push(s.charCodeAt(j), j); + } else { + for (let i = 0, j = from; i < count; i++, j -= 2) out.push(s.charCodeAt(j), j); + } + return out.join(","); +} +const word = "abcdefghijklmnop"; +console.log(codes(word, 4, 0, 3), "|", codes(word, 4, 15, -2)); +console.log(codes(word, 4, 0.5, 3), "|", codes(word, 3, 9.75, -2)); +const negativeZero = -0; +console.log(codes(word, 2, negativeZero, 3), Object.is(negativeZero, -0)); +console.log(codes(word, 2, NaN, 3), "|", codes(word, 2, Infinity, -2), "|", codes(word, 2, -Infinity, 3)); +// Near 2^52 the run-time test passes or fails on the trip count; near 2^53 +// the double counter rounds (odd values above 2^53 are not representable). +console.log(codes(word, 4, 2 ** 52 - 16, 3)); +console.log(codes(word, 6, 2 ** 52 - 4, 3)); +console.log(codes(word, 6, 2 ** 53 - 10, 3)); +console.log(codes(word, 4, -(2 ** 53) + 3, -2)); +// A limit that is not finite or not an integer. +function capped(s: string, limit: number): string { + const out: number[] = []; + for (let i = 0, j = 1; i < limit; i++, j += 2) { + if (j >= s.length) break; + out.push(s.charCodeAt(j)); + } + return out.join(","); +} +console.log(capped(word, Infinity), "|", capped(word, 3.5), "|", capped(word, NaN), "|", capped(word, -Infinity)); + +// A descending driver with an ascending follower, inclusive bounds and +// larger steps. +function mirror(s: string): string { + const out: string[] = []; + for (let i = s.length - 1, j = 0; i >= 0; i--, j += 2) out.push(`${s.charCodeAt(i)}@${j}`); + return out.join(" "); +} +console.log(mirror("scriptc")); +function stepped(bytes: Uint8Array, last: number): string { + const out: number[] = []; + for (let i = 0, j = bytes.length - 1; i <= last; i += 3, j -= 5) + out.push(bytes[i]! | 0, bytes[j]! | 0, j); + return out.join(","); +} +console.log(stepped(px, 9), "|", stepped(px, 30)); + +// Three counters, continue and break. +function triple(a: Uint8Array, n: number): string { + const out: number[] = []; + for (let i = 0, j = 10, k = 100; i < n; i++, j -= 1, k += 7) { + if (i % 3 === 1) continue; + if (k > 150) break; + out.push(a[i]!, a[j]!, k); + } + return out.join(","); +} +console.log(triple(px, 12), "|", triple(px, 4)); + +// A counted limit with a known range and a follower used by a remainder. +let rotated = 0; +for (let i = 0, j = 5; i < 1000; i++, j += 3) rotated = (rotated * 31 + px[j % 32]!) % 1000003; +console.log(rotated); + +// The follower starting from a driver's start of unproven exactness. +function window(bytes: Uint8Array, from: number, to: number): string { + const out: number[] = []; + for (let i = from, j = 0; i < to; i++, j += 2) out.push(bytes[i]! | 0, bytes[j]! | 0, i, j); + return out.join(","); +} +console.log(window(px, 2, 5), "|", window(px, 1.5, 4), "|", window(px, -0, 2)); + +// Counters a closure captures keep one binding per iteration; a follower the +// body writes, one the condition reads, and a start that reads another +// counter keep their JS values. +const reads: (() => number)[] = []; +for (let i = 0, j = 0; i < 3; i++, j += 4) { + reads.push(() => j); + void px[j]; +} +console.log(reads.map((read) => read()).join(",")); +const written: number[] = []; +for (let i = 0, j = 0; i < 5; i++, j += 2) { + if (i === 2) j += 7; + written.push(px[j]!, j); +} +console.log(written.join(",")); +const chased: number[] = []; +for (let i = 0, j = 5; i < j; i += 2, j++) chased.push(px[i]!, i, j); +console.log(chased.join(",")); +const offset: number[] = []; +for (let i = 1, j = i + 1; i < 4; i++, j += 2) offset.push(px[j]!, j); +console.log(offset.join(",")); + +// Counters that live across a yield or an await. +function* strided(bytes: Uint8Array, n: number): Generator { + for (let i = 0, j = 1; i < n; i++, j += 3) yield bytes[j]! + i; +} +console.log([...strided(px, 6)].join(",")); +async function weighted(bytes: Uint8Array, n: number): Promise { + let sum = 0; + for (let i = 0, j = 2; i < n; i++, j += 5) { + await null; + sum += bytes[j]! * (i + 1); + } + return sum; +} +void (async () => console.log(await weighted(px, 6), await weighted(px, 3)))(); diff --git a/tests/harness/corpus-costs/7141-comoving-loop-counters.ts.json b/tests/harness/corpus-costs/7141-comoving-loop-counters.ts.json new file mode 100644 index 0000000000..7c06068852 --- /dev/null +++ b/tests/harness/corpus-costs/7141-comoving-loop-counters.ts.json @@ -0,0 +1 @@ +{ "plain": 4260, "san": 6600 } From 17b70e6d6a02ca09bdb6f9214e73305731a03410 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 08:57:59 -0500 Subject: [PATCH 164/395] 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 0000000000..f5299cb9a0 --- /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 7a6a76cf9f..1831745556 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 0000000000..ae6c2ae233 --- /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 0000000000..5c265ea018 --- /dev/null +++ b/tests/harness/corpus-costs/7249-buffer-index-of-candidates.ts.json @@ -0,0 +1 @@ +{ "plain": 2870, "san": 3656 } From 1e0dabd74864c4c2f4e6c962cf550bad540047a6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:05:46 -0500 Subject: [PATCH 165/395] Declare every http2 listener event for once and addListener The lowering accepts on, once and addListener alike for each server, session and stream kind, but the fallback Node types (used when a project has no @types/node) declared fewer events for once than for on on the http2 servers, sessions and streams, and no addListener on any of them. `http2Server.once("connection", ...)` and `server.addListener("stream", ...)` failed to type-check (SC0001) where Node runs them. once and addListener on Http2Server, Http2SecureServer, Http2Session, Http2Stream, ServerHttp2Stream and ClientHttp2Stream now declare the same overloads as on. Corpus 7281 runs an HTTP CONNECT proxy on node:http, which #1791's declaration of the http.Server 'connect' event made compile: it tunnels to a local target and pipes both ways, with a non-empty head, once and addListener registrations, and an 'upgrade' handover. Corpus 7282 registers http2 server, session and stream listeners with once and addListener. Closes #1439 --- .../ambient/scriptc-node-fallback.d.ts | 142 +++++++++++++++++- .../7281-http-server-connect-tunnel.ts.json | 6 + .../7282-http2-once-add-listener.ts.json | 6 + .../corpus/7281-http-server-connect-tunnel.ts | 81 ++++++++++ tests/corpus/7282-http2-once-add-listener.ts | 54 +++++++ .../7281-http-server-connect-tunnel.ts.json | 1 + .../7282-http2-once-add-listener.ts.json | 1 + 7 files changed, 286 insertions(+), 5 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7281-http-server-connect-tunnel.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7282-http2-once-add-listener.ts.json create mode 100644 tests/corpus/7281-http-server-connect-tunnel.ts create mode 100644 tests/corpus/7282-http2-once-add-listener.ts create mode 100644 tests/harness/corpus-costs/7281-http-server-connect-tunnel.ts.json create mode 100644 tests/harness/corpus-costs/7282-http2-once-add-listener.ts.json diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 2a3f11be91..02b88a677c 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -5929,6 +5929,7 @@ declare module "http2" { export interface Http2Stream { on(event: "error" | "close", listener: (err: Error) => void): void; once(event: "error" | "close", listener: (err: Error) => void): void; + addListener(event: "error" | "close", listener: (err: Error) => void): void; destroy(): void; } export interface Http2ServerRequest { @@ -6029,11 +6030,6 @@ declare module "http2" { listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, ): void; on(event: "session", listener: (session: ServerHttp2Session) => void): void; - once( - event: "stream", - listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, - ): void; - once(event: "session", listener: (session: ServerHttp2Session) => void): void; /* HTTP CONNECT (and, in Node, the h2 extended CONNECT — which the * allowHTTP1 lowering never sees): the second argument is the raw * socket on HTTP/1.1 connections; narrow with instanceof. */ @@ -6051,6 +6047,42 @@ declare module "http2" { ): void; once(event: "error", listener: (err: Error) => void): void; once(event: "close", listener: () => void): void; + once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; + once( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + once(event: "session", listener: (session: ServerHttp2Session) => void): void; + once( + event: "connect", + listener: ( + req: Http2ServerRequest | import("http").IncomingMessage, + resOrSocket: Http2ServerResponse | Socket, + ) => void, + ): void; + addListener(event: "connection", listener: (socket: Socket) => void): void; + addListener( + event: "request", + listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener(event: "close", listener: () => void): void; + addListener( + event: "sessionError", + listener: (err: Error, session: ServerHttp2Session) => void, + ): void; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener(event: "session", listener: (session: ServerHttp2Session) => void): void; + addListener( + event: "connect", + listener: ( + req: Http2ServerRequest | import("http").IncomingMessage, + resOrSocket: Http2ServerResponse | Socket, + ) => void, + ): void; } export interface SecureServerOptions { allowHTTP1?: boolean; @@ -6392,6 +6424,28 @@ declare module "http2" { listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void, ): void; once(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void; + once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; + once(event: "ping", listener: (payload: Uint8Array) => void): void; + addListener(event: "close" | "timeout", listener: () => void): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "connect", + listener: (session: Http2Session, socket: import("net").Socket) => void, + ): void; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener( + event: "goaway", + listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void, + ): void; + addListener(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void; + addListener( + event: "frameError", + listener: (type: number, code: number, id: number) => void, + ): void; + addListener(event: "ping", listener: (payload: Uint8Array) => void): void; } export interface ClientHttp2Session extends Http2Session { @@ -6469,7 +6523,30 @@ declare module "http2" { listener: () => void, ): void; once(event: "error", listener: (err: Error) => void): void; + once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; + addListener(event: "data", listener: (chunk: any) => void): void; + addListener( + event: + | "end" + | "close" + | "aborted" + | "ready" + | "timeout" + | "drain" + | "finish" + | "wantTrailers", + listener: () => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "frameError", + listener: (type: number, code: number, id: number) => void, + ): void; + addListener( + event: "trailers", + listener: (trailers: IncomingHttpHeaders, flags: number) => void, + ): void; respond( headers?: OutgoingHttpHeaders, options?: { endStream?: boolean; waitForTrailers?: boolean }, @@ -6519,6 +6596,7 @@ declare module "http2" { on(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; once(event: "response", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; once(event: "push", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; + once(event: "headers", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; once(event: "data", listener: (chunk: any) => void): void; once( event: @@ -6533,6 +6611,42 @@ declare module "http2" { listener: () => void, ): void; once(event: "error", listener: (err: Error) => void): void; + once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; + once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; + addListener( + event: "response", + listener: (headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener( + event: "push", + listener: (headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener( + event: "headers", + listener: (headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener(event: "data", listener: (chunk: any) => void): void; + addListener( + event: + | "end" + | "close" + | "aborted" + | "ready" + | "timeout" + | "drain" + | "finish" + | "wantTrailers", + listener: () => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "frameError", + listener: (type: number, code: number, id: number) => void, + ): void; + addListener( + event: "trailers", + listener: (trailers: IncomingHttpHeaders, flags: number) => void, + ): void; } export interface Http2Server { @@ -6567,6 +6681,24 @@ declare module "http2" { ): void; once(event: "error", listener: (err: Error) => void): void; once(event: "close" | "listening" | "timeout", listener: () => void): void; + once(event: "connection", listener: (socket: import("net").Socket) => void): void; + once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener(event: "session", listener: (session: ServerHttp2Session) => void): void; + addListener( + event: "request", + listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener(event: "close" | "listening" | "timeout", listener: () => void): void; + addListener(event: "connection", listener: (socket: import("net").Socket) => void): void; + addListener( + event: "sessionError", + listener: (err: Error, session: ServerHttp2Session) => void, + ): void; } export interface ServerOptions { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7281-http-server-connect-tunnel.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7281-http-server-connect-tunnel.ts.json new file mode 100644 index 0000000000..2328d508b5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7281-http-server-connect-tunnel.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7281-http-server-connect-tunnel.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7282-http2-once-add-listener.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7282-http2-once-add-listener.ts.json new file mode 100644 index 0000000000..96c879197b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7282-http2-once-add-listener.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7282-http2-once-add-listener.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7281-http-server-connect-tunnel.ts b/tests/corpus/7281-http-server-connect-tunnel.ts new file mode 100644 index 0000000000..d8b396c074 --- /dev/null +++ b/tests/corpus/7281-http-server-connect-tunnel.ts @@ -0,0 +1,81 @@ +// An HTTP CONNECT proxy on node:http: the server's 'connect' listener gets +// the request, the raw client socket and the bytes read past the request +// head, opens a TCP connection to the requested host and pipes both ways. +// 'once' and 'addListener' register the same event, and 'upgrade' hands +// over the socket for an Upgrade request. +import * as http from "node:http"; +import * as net from "node:net"; + +const target = net.createServer((socket) => { + socket.on("data", (d) => socket.write(`echo:${d.toString()}`)); +}); + +const proxy = http.createServer((_req, res) => res.end("plain")); +const seen: string[] = []; +proxy.on("connect", (req, clientSocket, head) => { + seen.push(`connect ${req.method} ${req.url} head=${head.length}`); + const [host, port] = (req.url ?? "").split(":"); + const upstream = net.connect(Number(port), host, () => { + clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + if (head.length > 0) upstream.write(head); + upstream.pipe(clientSocket); + clientSocket.pipe(upstream); + }); +}); +proxy.once("connect", (req) => seen.push(`once ${req.url}`)); +proxy.addListener("upgrade", (req, socket, head) => { + seen.push(`upgrade ${req.headers.upgrade} head=${head.length}`); + socket.end("HTTP/1.1 101 Switching Protocols\r\nUpgrade: echo\r\nConnection: Upgrade\r\n\r\n"); +}); + +function tunnel( + proxyPort: number, + targetPort: number, + payload: string, + withHead: boolean, +): Promise { + return new Promise((resolve) => { + const c = net.connect(proxyPort, "127.0.0.1", () => { + const request = `CONNECT 127.0.0.1:${targetPort} HTTP/1.1\r\nHost: 127.0.0.1:${targetPort}\r\n\r\n`; + c.write(withHead ? request + payload : request); + }); + let text = ""; + let sent = withHead; + c.on("data", (d) => { + text += d.toString(); + if (!sent && text.includes("\r\n\r\n")) { + sent = true; + c.write(payload); + } + if (sent && text.includes(`echo:${payload}`)) { + c.end(); + resolve(text); + } + }); + }); +} + +function upgrade(proxyPort: number): Promise { + return new Promise((resolve) => { + const u = net.connect(proxyPort, "127.0.0.1", () => { + u.write("GET /ws HTTP/1.1\r\nHost: x\r\nConnection: Upgrade\r\nUpgrade: echo\r\n\r\n"); + }); + let text = ""; + u.on("data", (d) => (text += d.toString())); + u.on("end", () => resolve(text.split("\r\n")[0] ?? "")); + }); +} + +target.listen(0, "127.0.0.1", () => { + const targetPort = (target.address() as net.AddressInfo).port; + proxy.listen(0, "127.0.0.1", async () => { + const proxyPort = (proxy.address() as net.AddressInfo).port; + console.log(JSON.stringify(await tunnel(proxyPort, targetPort, "first", false))); + console.log(JSON.stringify(await tunnel(proxyPort, targetPort, "second", false))); + console.log(JSON.stringify(await tunnel(proxyPort, targetPort, "with-head", true))); + console.log(JSON.stringify(await upgrade(proxyPort))); + console.log(seen.join("\n").replaceAll(String(targetPort), "PORT")); + proxy.close(); + target.close(); + }); +}); diff --git a/tests/corpus/7282-http2-once-add-listener.ts b/tests/corpus/7282-http2-once-add-listener.ts new file mode 100644 index 0000000000..ab1e24a9f0 --- /dev/null +++ b/tests/corpus/7282-http2-once-add-listener.ts @@ -0,0 +1,54 @@ +// node:http2 listeners registered with once and addListener on the server, +// the client session and both stream kinds. A once listener runs for the +// first event only; addListener is on. +import * as http2 from "node:http2"; +import * as net from "node:net"; + +const log: string[] = []; +const accepted: string[] = []; +const server = http2.createServer(); +server.once("connection", (socket: net.Socket) => accepted.push(`connection ${typeof socket.remotePort}`)); +server.addListener("session", () => accepted.push("session")); +server.once("stream", () => log.push("server first stream")); +server.addListener("stream", (stream, headers) => { + const header = headers[":path"]; + const path = typeof header === "string" ? header : ""; + log.push(`server stream ${path}`); + let body = ""; + stream.addListener("data", (chunk) => (body += chunk.toString())); + stream.once("end", () => { + stream.respond({ ":status": 200, "x-path": path }); + stream.end(`${path}:${body}`); + }); +}); +server.once("close", () => log.push("server close")); + +function request(client: http2.ClientHttp2Session, path: string, body: string): Promise { + return new Promise((resolve) => { + const req = client.request({ ":path": path, ":method": "POST" }); + req.once("response", (headers) => { + const status = headers[":status"]; + const echoed = headers["x-path"]; + log.push(`client response ${typeof status === "number" ? status : "?"} ${typeof echoed === "string" ? echoed : "?"}`); + }); + let text = ""; + req.addListener("data", (chunk) => (text += chunk.toString())); + req.once("end", () => resolve(text)); + req.end(body); + }); +} + +server.listen(0, () => { + const port = (server.address() as net.AddressInfo).port; + const client = http2.connect(`http://localhost:${port}`); + client.once("connect", () => log.push("client connect")); + client.addListener("close", () => { + log.push("client close"); + server.close(() => console.log([accepted.sort().join(", "), ...log].join("\n"))); + }); + void (async () => { + console.log(await request(client, "/a", "one")); + console.log(await request(client, "/b", "two")); + client.close(); + })(); +}); diff --git a/tests/harness/corpus-costs/7281-http-server-connect-tunnel.ts.json b/tests/harness/corpus-costs/7281-http-server-connect-tunnel.ts.json new file mode 100644 index 0000000000..c35274f8c6 --- /dev/null +++ b/tests/harness/corpus-costs/7281-http-server-connect-tunnel.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 3100 } diff --git a/tests/harness/corpus-costs/7282-http2-once-add-listener.ts.json b/tests/harness/corpus-costs/7282-http2-once-add-listener.ts.json new file mode 100644 index 0000000000..9e54078334 --- /dev/null +++ b/tests/harness/corpus-costs/7282-http2-once-add-listener.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 5100 } From 20f4d1ff125804302495c2f357f1f10cfa5efa3d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:08:35 -0500 Subject: [PATCH 166/395] 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 c84ce3e52b..971065408b 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 574c49ff4f..c341da852b 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 31ce5da022..9393fcaa80 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 a59d6365aa..06aad26cfd 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 2a3f11be91..519608393e 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 9a65d9a242..23f0a0291c 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 3e97d0c101..236ce12cfa 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 7a1791197e..47e66f9b26 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 0167adf129..cb16fd257f 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 16db45b6c5..e98ae0bd90 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 fcdbe86947..edadaa2443 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 5b984f318d..ec2edb931a 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 fb05751218..e8fad6dd1b 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 45b3edbaa9..d6eb69f6ff 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 0000000000..c0e5f98120 --- /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 0000000000..1f37c6b647 --- /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 0000000000..722ebaacbf --- /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 0000000000..9d121895af --- /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 c8a10a731a..00d229bad2 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 @@ ScrDyn *scr_crypto_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn /* ── 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 546e3d75bc..0e6c6e07f0 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 cf44338f56..dfa6fd2f37 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 0000000000..80edfd0298 --- /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 0000000000..fe9c59bcc3 --- /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 0000000000..79edcc502a --- /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 0000000000..ff32e6e093 --- /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 644e901f4e..27e68a6188 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 133978b5ef..cabce49c4b 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 0000000000..40196decf2 --- /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 0000000000..6a82dd0819 --- /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 0000000000..4df2be4364 --- /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 0000000000..d20dc198ad --- /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 b5fb028f90..c8b0b51b8d 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 0318671352edbc3d699dc9beeaca53316af149d6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:17:49 -0500 Subject: [PATCH 167/395] Reuse the class dispatch passes' per-function scans while bodies are unchanged --- .../lowering/caught-value-dispatch.ts | 24 +- .../frontend/lowering/class-construction.ts | 20 +- .../lowering/class-dynamic-dispatch.ts | 118 ++------ .../deferred-module-initializers.test.ts | 9 + .../lowering/deferred-module-initializers.ts | 14 +- .../frontend/lowering/dispatch-sites.test.ts | 60 ++++ .../src/frontend/lowering/dispatch-sites.ts | 272 ++++++++++++++++++ .../lowering/generator-dynamic-dispatch.ts | 50 ++-- .../compiler/src/frontend/lowering/lowerer.ts | 18 +- 9 files changed, 417 insertions(+), 168 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/dispatch-sites.test.ts create mode 100644 packages/compiler/src/frontend/lowering/dispatch-sites.ts diff --git a/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts b/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts index 606d10d563..07d68baecf 100644 --- a/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/caught-value-dispatch.ts @@ -11,6 +11,7 @@ import { } from "../../ir/ir.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; import { classInstanceOf } from "./class-dynamic-dispatch.js"; +import type { DispatchSites } from "./dispatch-sites.js"; import type { ClassInfo } from "./lower-classes.js"; import type { Lowerer } from "./lowerer.js"; @@ -26,25 +27,16 @@ export class CaughtValueDispatch { private readonly payloadHelpers = new Map(); private readonly payloadHelperFns = new Set(); - process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean { + process(lowerer: Lowerer, functions: readonly IrFunction[], sites: DispatchSites): boolean { const consumers: IrFunction[] = []; for (const fn of functions) { if (fn === this.helper) continue; - let consumes = false; - everyStmtList(fn.body, { - stmt: (stmt) => { - if (stmt.kind === "throw" && stmt.value.type.kind === "object") { - const info = lowerer.classes.get(stmt.value.type.className); - if (info) this.thrownClasses.add(info.def.name); - } - return true; - }, - expr: (expr) => { - if (expr.kind === "caughtToDyn") consumes = true; - return true; - }, - }); - if (consumes) consumers.push(fn); + const found = sites.of(fn); + for (const className of found.thrown) { + const info = lowerer.classes.get(className); + if (info) this.thrownClasses.add(info.def.name); + } + if (found.caughtValues) consumers.push(fn); } if (!this.helper && consumers.length === 0) return false; let changed = false; diff --git a/packages/compiler/src/frontend/lowering/class-construction.ts b/packages/compiler/src/frontend/lowering/class-construction.ts index 30f504b4a5..66ec547bc3 100644 --- a/packages/compiler/src/frontend/lowering/class-construction.ts +++ b/packages/compiler/src/frontend/lowering/class-construction.ts @@ -13,7 +13,8 @@ import { type IrType, type SrcLoc, } from "../../ir/ir.js"; -import { everyStmtList, transformExpr, transformStmtList } from "../../ir/traverse.js"; +import { transformExpr, transformStmtList } from "../../ir/traverse.js"; +import type { DispatchSites } from "./dispatch-sites.js"; import type { Lowerer } from "./lowerer.js"; import type { ParamShape } from "./call-signatures.js"; import { classPrototypeData } from "./class-prototypes.js"; @@ -254,22 +255,17 @@ export class ClassConstructionDispatch { private prototypeHelper: IrFunction | null = null; private readonly prototypes = new Set(); - process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean { + process(lowerer: Lowerer, functions: readonly IrFunction[], sites: DispatchSites): boolean { const discover = (type: IrType): void => { if (type.kind === "classval") this.constructors.add(type.className); else if (type.kind === "union") lowerer.unions.get(type.unionId)?.arms.forEach(discover); }; const prototypeReads = new Set(); - for (const fn of functions) - everyStmtList(fn.body, { - stmt: () => true, - expr: (expr) => { - if (expr.kind === "dynFrom") discover(expr.value.type); - if (expr.kind === "libCall" && expr.fn === "dyn.classBasePrototype") - prototypeReads.add(fn); - return true; - }, - }); + for (const fn of functions) { + const found = sites.of(fn); + found.values.forEach(discover); + if (found.basePrototypes) prototypeReads.add(fn); + } let changed = false; for (const fn of functions) { if (fn === this.prototypeHelper || !prototypeReads.has(fn)) continue; diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index fefe85d0c4..f2ccb0ec6a 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -77,6 +77,7 @@ import { isClassCallback } from "./class-callbacks.js"; import { CaughtValueDispatch } from "./caught-value-dispatch.js"; import { refreshDescriptorGuards } from "./class-descriptors.js"; import { ClassConstructionDispatch } from "./class-construction.js"; +import { DispatchSites } from "./dispatch-sites.js"; import { emitterRooted, lowerDynamicEmitterEmit, @@ -226,7 +227,7 @@ const KEY_CALLS = new Map([ ["dyn.keySetComputed", "value"], ]); -function toPrimitiveHint(expr: IrExpr): ToPrimitiveHint | null { +export function toPrimitiveHint(expr: IrExpr): ToPrimitiveHint | null { if (expr.kind === "toString") return expr.operand.type.kind === "dyn" ? "string" : null; if (expr.kind === "dynKeyGet") return expr.key.type.kind === "dyn" ? "string" : null; if (expr.kind !== "libCall") return null; @@ -780,6 +781,8 @@ export class ClassDynamicDispatch { private readonly generatorDispatch = new GeneratorDynamicDispatch(); private readonly caughtDispatch = new CaughtValueDispatch(); private readonly constructionDispatch = new ClassConstructionDispatch(); + /** What each function's body holds for this pass and its sub-passes. */ + readonly sites = new DispatchSites(); /** Property names a delete can remove (null: any name), for the field * creation tracking decision. */ private deletedNames: Set | null | undefined; @@ -862,12 +865,13 @@ export class ClassDynamicDispatch { ) fn.body.shift(); } - let changed = this.caughtDispatch.process(lowerer, functions); + let changed = this.caughtDispatch.process(lowerer, functions, this.sites); changed = refreshErrorMethodDispatch(lowerer, functions) || changed; changed = refreshClassMethodValueSelections(lowerer) || changed; changed = refreshErrorPropertyDispatch(lowerer, functions) || changed; - changed = this.generatorDispatch.process(lowerer, functions, this.generated) || changed; - changed = this.constructionDispatch.process(lowerer, functions) || changed; + changed = + this.generatorDispatch.process(lowerer, functions, this.generated, this.sites) || changed; + changed = this.constructionDispatch.process(lowerer, functions, this.sites) || changed; const seenTypes = new Set(); const rewrite = new Set(); const toPrimitiveSites = new Set(); @@ -891,100 +895,18 @@ export class ClassDynamicDispatch { }; const boxedBefore = this.boxed.size; const boxedConstructorsBefore = this.boxedConstructors.size; - for (const fn of functions) - everyStmtList(fn.body, { - stmt: (statement) => { - // Field-creation instrumentation can box a base-constructor receiver - // even when only its derived instances otherwise cross the boundary. - if ( - statement.kind === "fieldSet" && - lowerer.classes.get(statement.className)?.def.tracksOwnFields && - isClassOwnEnumerableFieldName(statement.field) - ) - discover(statement.obj.type); - return true; - }, - expr: (expr) => { - if ( - expr.kind === "jsonStringify" || - (expr.kind === "libCall" && - (expr.fn === "json.stringifyValue" || expr.fn === "json.stringifyReplacer")) - ) { - this.serializesClasses = true; - } - if ( - toPrimitiveHint(expr) !== null || - (expr.kind === "libCall" && (expr.fn === "dyn.valueOf" || expr.fn === "dyn.toString")) - ) - toPrimitiveSites.add(fn); - switch (expr.kind) { - case "dynFrom": - discover(expr.value.type); - break; - case "fieldIncDec": - if ( - lowerer.classes.get(expr.className)?.def.tracksOwnFields && - isClassOwnEnumerableFieldName(expr.field) - ) - discover(expr.obj.type); - break; - case "call": - if ( - (expr.callee === "%dyn.class.properties" || - expr.callee === "%dyn.class.readProperties") && - expr.args[0]?.kind === "dynFrom" && - isDynTypedRefType(expr.args[0].value.type) - ) - rewrite.add(fn); - break; - case "dynHasKey": - hasKeySites.add(fn); - break; - case "caughtTest": - if (expr.test === "instanceof") caughtSites.add(fn); - break; - case "caughtNarrow": - case "caughtCheck": - if (expr.type.kind === "object") caughtSites.add(fn); - break; - case "dynKeyGet": - case "dynInvoke": - rewrite.add(fn); - break; - case "dynCheck": - if (isDynTypedRefType(expr.type)) rewrite.add(fn); - // A dynamic function checked into a native function type - // receives its arguments as dynamic values: class instances - // passed to it cross the boundary when it is called (a JS - // `list.map((rec) => rec.match(x))` callback with an untyped - // parameter). - if (expr.type.kind === "func") expr.type.params.forEach(discover); - break; - case "libCall": - if ( - expr.fn === "bytes.construct" || - expr.fn === "dyn.keySet" || - expr.fn === "dyn.keySetComputed" || - expr.fn === "dyn.iterator" || - expr.fn === "dyn.arrayFromIterator" || - expr.fn === "dyn.toString" || - expr.fn === "dyn.stringConstructor" || - expr.fn === "dyn.forInKeys" || - expr.fn === "dyn.forInKeyStrings" || - expr.fn === "dyn.hasKeyComputed" || - expr.fn === "dyn.hasOwnComputed" || - expr.fn === "dyn.propertyIsEnumerableComputed" || - expr.fn === "dyn.defineProperty" || - expr.fn === "dyn.getPrototype" || - expr.fn === "dyn.reflectGet" || - expr.fn === "dyn.reflectSet" - ) - rewrite.add(fn); - break; - } - return true; - }, - }); + for (const fn of functions) { + const found = this.sites.of(fn); + // A class starts tracking its own fields during this pass. + for (const { type, fieldClass } of found.boxing) + if (fieldClass === undefined || lowerer.classes.get(fieldClass)?.def.tracksOwnFields) + discover(type); + if (found.serializes) this.serializesClasses = true; + if (found.conversions) toPrimitiveSites.add(fn); + if (found.dispatches) rewrite.add(fn); + if (found.keyTests) hasKeySites.add(fn); + if (found.caughtClassTests) caughtSites.add(fn); + } // The classes this pass found join the tables in a fixed order, not in // the order the scan met them. sortAdded(lowerer, this.boxed, boxedBefore); diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts index 28f05bfce8..4696659bc0 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.test.ts @@ -159,6 +159,15 @@ test("fills every empty copy with the exact file and span while preserving popul for (const untouched of [otherFile, otherStart, otherEnd]) expect(untouched.body).toEqual([]); }); +test("reports the functions whose placeholders it fills", () => { + const { initializers, pending, lowerer } = fixture(); + const functions = [fn("init", [block(loc)]), fn("use", [read("registry")])]; + pending.set("registry", [{ loc, lower: () => [value(4)] }]); + const filled: IrFunction[] = []; + expect(initializers.process(lowerer, functions, (f) => filled.push(f))).toBe(true); + expect(filled).toEqual([functions[0]]); +}); + test("an update of the binding demands its initializer", () => { const { initializers, pending, lowerer } = fixture(); const updated = block(loc), diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts index eb22b40855..a44df8f750 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts @@ -129,7 +129,14 @@ export class DeferredModuleInitializers { return block; } - process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean { + /** Materializes the initializers that reached bodies observe. `filled` + * receives each function whose body changes inside an unchanged statement + * list. */ + process( + lowerer: Lowerer, + functions: readonly IrFunction[], + filled?: (fn: IrFunction) => void, + ): boolean { if (this.pending.size === 0) return false; // Scans keep only the reads of deferred bindings. for (const key of this.pending.keys()) { @@ -192,7 +199,10 @@ export class DeferredModuleInitializers { } if (holders.length === 0) continue; // These bodies changed inside unchanged statement lists. - for (const fn of holders) this.scans.delete(fn); + for (const fn of holders) { + this.scans.delete(fn); + filled?.(fn); + } // An inserted body can introduce a placeholder needed by another // action in this pass. Unattached bodies are not part of the live IR. for (const nested of scanStatements(body).placeholders) add(nested, holders); diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.test.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.test.ts new file mode 100644 index 0000000000..1d027ac34d --- /dev/null +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.test.ts @@ -0,0 +1,60 @@ +import { expect, test, vi } from "vitest"; +import { DYN, F64, VOID, type IrFunction, type IrStmt, type SrcLoc } from "../../ir/ir.js"; +import * as traversal from "../../ir/traverse.js"; +import { DispatchSites } from "./dispatch-sites.js"; + +const loc: SrcLoc = { file: "sites.ts", start: 0, end: 1 }; +const keyRead = (key: string): IrStmt => ({ + kind: "exprStmt", + expr: { + kind: "dynKeyGet", + value: { kind: "varRef", localId: "value", type: DYN, loc }, + key: { kind: "strLit", value: key, type: DYN, loc }, + type: DYN, + loc, + }, + loc, +}); +const value = (n: number): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "numLit", value: n, type: F64, loc }, + loc, +}); +const fn = (body: IrStmt[]): IrFunction => ({ + name: "f", + body, + params: [], + locals: [], + returnType: VOID, + loc, +}); + +test("reuses a function's sites while its statement list is unchanged", () => { + const sites = new DispatchSites(); + const f = fn([value(1)]); + const walk = vi.spyOn(traversal, "everyStmtList"); + try { + expect(sites.of(f).dispatches).toBe(false); + expect(sites.of(f).dispatches).toBe(false); + expect(walk).toHaveBeenCalledTimes(1); + // A statement added to the same list is found. + f.body.push(keyRead("next")); + expect(sites.of(f)).toMatchObject({ dispatches: true, generatorSteps: true }); + // So is a replaced list. + f.body = [value(2)]; + expect(sites.of(f)).toMatchObject({ dispatches: false, generatorSteps: false }); + expect(walk).toHaveBeenCalledTimes(3); + } finally { + walk.mockRestore(); + } +}); + +test("scans a body again once a change inside an unchanged list is reported", () => { + const sites = new DispatchSites(); + const placeholder: Extract = { kind: "block", body: [], loc }; + const f = fn([placeholder]); + expect(sites.of(f).dispatches).toBe(false); + placeholder.body.push(keyRead("map")); + sites.forget(f); + expect(sites.of(f)).toMatchObject({ dispatches: true, runtimeSteps: true }); +}); diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts new file mode 100644 index 0000000000..2618dacf9a --- /dev/null +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -0,0 +1,272 @@ +import { InternalCompilerError } from "../../errors.js"; +import { + isClassOwnEnumerableFieldName, + isDynTypedRefType, + typeKey, + type IrFunction, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; +import { everyStmtList } from "../../ir/traverse.js"; +import { toPrimitiveHint } from "./class-dynamic-dispatch.js"; +import { GENERATOR_STEP_METHODS, HELPER_METHODS } from "./generator-dynamic-dispatch.js"; + +/** 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. */ +export interface FunctionSites { + statements: IrStmt[]; + length: number; + first: IrStmt | undefined; + last: IrStmt | undefined; + /** The value types of `dynFrom` expressions, once per type key. */ + values: IrType[]; + /** The types the class dispatch scan boxes, once per type key and class: + * `dynFrom` values, the parameter types of dynamic functions checked into + * native function types, and the receivers of own enumerable field writes + * and updates. A receiver counts only while its class (`fieldClass`) + * tracks its own fields. */ + boxing: { type: IrType; fieldClass?: string }[]; + /** JSON serialization of dynamic values. */ + serializes: boolean; + /** ToPrimitive conversions, and `dyn.valueOf` and `dyn.toString` calls. */ + conversions: boolean; + /** Sites the class dispatch rewrite can change. */ + dispatches: boolean; + /** `dynHasKey` tests. */ + keyTests: boolean; + /** `instanceof` tests of caught values, and caught values narrowed or + * checked to class types. */ + caughtClassTests: boolean; + /** Calls and reads of generator steps, and reads of iterator helper + * methods. */ + generatorSteps: boolean; + /** Values the runtime steps: iterator helper calls and reads, `dynIterN`, + * and rest destructuring. */ + runtimeSteps: boolean; + /** `dyn.classBasePrototype` calls. */ + basePrototypes: boolean; + /** The classes of thrown class instances, once each. */ + thrown: string[]; + /** `caughtToDyn` conversions. */ + caughtValues: boolean; +} + +/** The `libCall`s the class dispatch rewrite can change. */ +const DISPATCH_LIB_CALLS = new Set([ + "bytes.construct", + "dyn.keySet", + "dyn.keySetComputed", + "dyn.iterator", + "dyn.arrayFromIterator", + "dyn.toString", + "dyn.stringConstructor", + "dyn.forInKeys", + "dyn.forInKeyStrings", + "dyn.hasKeyComputed", + "dyn.hasOwnComputed", + "dyn.propertyIsEnumerableComputed", + "dyn.defineProperty", + "dyn.getPrototype", + "dyn.reflectGet", + "dyn.reflectSet", +]); + +/** Prototype data helpers (classPrototypeData) gain method slots inside + * their initialization block, so their summaries are not reused. */ +const PROTOTYPE_DATA_PREFIX = "%prototype.data."; + +/** SCRIPTC_CHECK_DISPATCH_SCANS=1 checks each reused summary against a fresh walk. */ +const CHECK_SCANS = process.env["SCRIPTC_CHECK_DISPATCH_SCANS"] === "1"; + +/** The function summaries (FunctionSites) of one class dispatch pass, kept + * across its runs. */ +export class DispatchSites { + private readonly sites = new Map(); + + /** The sites of `fn`. A summary is reused while the function's statement + * list is the same array with the same length and the same first and last + * statements: the dispatch passes replace the lists they rewrite and add + * statements to the helpers they extend. Other changes inside an + * unchanged list are reported with `forget`, except in prototype data + * helpers, which are scanned on every use. */ + of(fn: IrFunction): FunctionSites { + const cached = this.sites.get(fn); + if ( + cached !== undefined && + !fn.name.startsWith(PROTOTYPE_DATA_PREFIX) && + cached.statements === fn.body && + cached.length === fn.body.length && + cached.first === fn.body[0] && + cached.last === fn.body[fn.body.length - 1] + ) { + if (CHECK_SCANS && !sameSites(cached, scanSites(fn.body))) + throw new InternalCompilerError( + `reused dispatch sites of ${fn.name} disagree with a fresh scan`, + ); + return cached; + } + const sites = scanSites(fn.body); + this.sites.set(fn, sites); + return sites; + } + + /** Drops the summary of a function whose body changed inside an unchanged + * statement list. */ + forget(fn: IrFunction): void { + this.sites.delete(fn); + } +} + +function scanSites(statements: IrStmt[]): FunctionSites { + const sites: FunctionSites = { + statements, + length: statements.length, + first: statements[0], + last: statements[statements.length - 1], + values: [], + boxing: [], + serializes: false, + conversions: false, + dispatches: false, + keyTests: false, + caughtClassTests: false, + generatorSteps: false, + runtimeSteps: false, + basePrototypes: false, + thrown: [], + caughtValues: false, + }; + const valueKeys = new Set(); + const boxingKeys = new Set(); + const thrown = new Set(); + const box = (type: IrType, fieldClass?: string): void => { + const key = `${fieldClass ?? ""}\0${typeKey(type)}`; + if (boxingKeys.has(key)) return; + boxingKeys.add(key); + sites.boxing.push(fieldClass === undefined ? { type } : { type, fieldClass }); + }; + everyStmtList(statements, { + stmt: (statement) => { + if (statement.kind === "throw" && statement.value.type.kind === "object") { + const className = statement.value.type.className; + if (!thrown.has(className)) { + thrown.add(className); + sites.thrown.push(className); + } + } + // Field-creation instrumentation can box a base-constructor receiver + // even when only its derived instances otherwise cross the boundary. + if (statement.kind === "fieldSet" && isClassOwnEnumerableFieldName(statement.field)) + box(statement.obj.type, statement.className); + return true; + }, + expr: (expr) => { + if ( + expr.kind === "jsonStringify" || + (expr.kind === "libCall" && + (expr.fn === "json.stringifyValue" || expr.fn === "json.stringifyReplacer")) + ) + sites.serializes = true; + if ( + toPrimitiveHint(expr) !== null || + (expr.kind === "libCall" && (expr.fn === "dyn.valueOf" || expr.fn === "dyn.toString")) + ) + sites.conversions = true; + if ( + (expr.kind === "dynInvoke" && GENERATOR_STEP_METHODS.has(expr.method)) || + (expr.kind === "dynKeyGet" && + expr.key.kind === "strLit" && + (GENERATOR_STEP_METHODS.has(expr.key.value) || HELPER_METHODS.has(expr.key.value))) + ) + sites.generatorSteps = true; + if ( + (expr.kind === "dynInvoke" && HELPER_METHODS.has(expr.method)) || + (expr.kind === "dynKeyGet" && + expr.key.kind === "strLit" && + HELPER_METHODS.has(expr.key.value)) || + expr.kind === "dynIterN" || + (expr.kind === "libCall" && expr.fn === "dyn.destructureRest") + ) + sites.runtimeSteps = true; + switch (expr.kind) { + case "dynFrom": { + const key = typeKey(expr.value.type); + if (!valueKeys.has(key)) { + valueKeys.add(key); + sites.values.push(expr.value.type); + } + box(expr.value.type); + break; + } + case "fieldIncDec": + if (isClassOwnEnumerableFieldName(expr.field)) box(expr.obj.type, expr.className); + break; + case "call": + if ( + (expr.callee === "%dyn.class.properties" || + expr.callee === "%dyn.class.readProperties") && + expr.args[0]?.kind === "dynFrom" && + isDynTypedRefType(expr.args[0].value.type) + ) + sites.dispatches = true; + break; + case "dynHasKey": + sites.keyTests = true; + break; + case "caughtTest": + if (expr.test === "instanceof") sites.caughtClassTests = true; + break; + case "caughtNarrow": + case "caughtCheck": + if (expr.type.kind === "object") sites.caughtClassTests = true; + break; + case "caughtToDyn": + sites.caughtValues = true; + break; + case "dynKeyGet": + case "dynInvoke": + sites.dispatches = true; + break; + case "dynCheck": + if (isDynTypedRefType(expr.type)) sites.dispatches = true; + // A dynamic function checked into a native function type + // receives its arguments as dynamic values: class instances + // passed to it cross the boundary when it is called (a JS + // `list.map((rec) => rec.match(x))` callback with an untyped + // parameter). + if (expr.type.kind === "func") expr.type.params.forEach((type) => box(type)); + break; + case "libCall": + if (expr.fn === "dyn.classBasePrototype") sites.basePrototypes = true; + if (DISPATCH_LIB_CALLS.has(expr.fn)) sites.dispatches = true; + break; + } + return true; + }, + }); + return sites; +} + +function sameSites(left: FunctionSites, right: FunctionSites): boolean { + const sameList = (a: readonly T[], b: readonly T[], same: (x: T, y: T) => boolean): boolean => + a.length === b.length && a.every((item, index) => same(item, b[index]!)); + return ( + sameList(left.values, right.values, (a, b) => a === b) && + sameList( + left.boxing, + right.boxing, + (a, b) => a.type === b.type && a.fieldClass === b.fieldClass, + ) && + sameList(left.thrown, right.thrown, (a, b) => a === b) && + left.serializes === right.serializes && + left.conversions === right.conversions && + left.dispatches === right.dispatches && + left.keyTests === right.keyTests && + left.caughtClassTests === right.caughtClassTests && + left.generatorSteps === right.generatorSteps && + left.runtimeSteps === right.runtimeSteps && + left.basePrototypes === right.basePrototypes && + left.caughtValues === right.caughtValues + ); +} diff --git a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts index 3f43e5e051..db542412fb 100644 --- a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts @@ -13,16 +13,20 @@ import { type IrStmt, type IrType, } from "../../ir/ir.js"; -import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; +import { transformStmtList } from "../../ir/traverse.js"; import { genResultRecord } from "../type-mapper.js"; +import type { DispatchSites } from "./dispatch-sites.js"; import type { Lowerer } from "./lowerer.js"; type GeneratorType = Extract; type Invoke = Extract; type KeyGet = Extract; +/** The generator methods a dynamic call or read steps. */ +export const GENERATOR_STEP_METHODS = new Set(["next", "return", "throw"]); + /** The iterator helper methods the runtime answers on checked iterators. */ -const HELPER_METHODS = new Set([ +export const HELPER_METHODS = new Set([ "map", "filter", "take", @@ -101,6 +105,7 @@ export class GeneratorDynamicDispatch { lowerer: Lowerer, functions: readonly IrFunction[], classHelpers: ReadonlySet, + sites: DispatchSites, ): boolean { const seen = new Set(); const rewrite = new Set(); @@ -117,34 +122,14 @@ export class GeneratorDynamicDispatch { lowerer.shapes.get(type.shapeId)?.fields.forEach((field) => discover(field.type)); else if (type.kind === "union") lowerer.unions.get(type.unionId)?.arms.forEach(discover); }; - for (const fn of functions) - everyStmtList(fn.body, { - stmt: () => true, - expr: (expr) => { - if (expr.kind === "dynFrom") discover(expr.value.type); - if (expr.kind === "dynInvoke" && ["next", "return", "throw"].includes(expr.method)) - rewrite.add(fn); - // The runtime steps the values iterator helpers and destructuring - // receive. - if ( - (expr.kind === "dynInvoke" && HELPER_METHODS.has(expr.method)) || - (expr.kind === "dynKeyGet" && - expr.key.kind === "strLit" && - HELPER_METHODS.has(expr.key.value)) || - expr.kind === "dynIterN" || - (expr.kind === "libCall" && expr.fn === "dyn.destructureRest") - ) - runtimeSteps = true; - if ( - expr.kind === "dynKeyGet" && - expr.key.kind === "strLit" && - (["next", "return", "throw"].includes(expr.key.value) || - HELPER_METHODS.has(expr.key.value)) - ) - rewrite.add(fn); - return true; - }, - }); + for (const fn of functions) { + const found = sites.of(fn); + found.values.forEach(discover); + // The runtime steps the values iterator helpers and destructuring + // receive. + if (found.runtimeSteps) runtimeSteps = true; + if (found.generatorSteps) rewrite.add(fn); + } let changed = false; for (const fn of functions) { // The generator helper's fallback may itself dispatch native classes. @@ -159,7 +144,7 @@ export class GeneratorDynamicDispatch { if ( expr.kind === "dynKeyGet" && expr.key.kind === "strLit" && - (["next", "return", "throw"].includes(expr.key.value) || + (GENERATOR_STEP_METHODS.has(expr.key.value) || (HELPER_METHODS.has(expr.key.value) && this.generators.size > 0)) ) { const method = expr.key.value; @@ -173,8 +158,7 @@ export class GeneratorDynamicDispatch { loc: expr.loc, }; } - if (expr.kind !== "dynInvoke" || !["next", "return", "throw"].includes(expr.method)) - return expr; + if (expr.kind !== "dynInvoke" || !GENERATOR_STEP_METHODS.has(expr.method)) return expr; const key = JSON.stringify([expr.method, expr.args.length, expr.calleeName]); let dispatch = this.dispatches.get(key); if (!dispatch) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index b02a0a843c..7a2bfd9e7c 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -8231,13 +8231,17 @@ export class Lowerer { for (;;) { drainInstances(); const declared = unitsInOrder(); - const initializerChanged = this.deferredModuleInitializers.process(this, [ - ...declared, - ...initFunctions, - ...instanceFunctions, - ...byName(this.liftedFns), - ...byName(this.implicitFns), - ]); + const initializerChanged = this.deferredModuleInitializers.process( + this, + [ + ...declared, + ...initFunctions, + ...instanceFunctions, + ...byName(this.liftedFns), + ...byName(this.implicitFns), + ], + (fn) => classDispatch.sites.forget(fn), + ); const dispatchChanged = classDispatch.process(this, [ ...declared, ...initFunctions, From ed66338cb6a2e47c7dfd409487b4fd22853973d5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:25:44 -0500 Subject: [PATCH 168/395] Record the order-parity baseline for the byte-mode chunk fence fixture --- .../tests/diagnostics/stream-byte-mode-chunk-fences.ts.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-byte-mode-chunk-fences.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-byte-mode-chunk-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-byte-mode-chunk-fences.ts.json new file mode 100644 index 0000000000..8a02db2bee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-byte-mode-chunk-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/stream-byte-mode-chunk-fences.ts" + ], + "diags": [] +} From 091f280ceced8d2df2e7e1745f6e3527b797a1c4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:32:19 -0500 Subject: [PATCH 169/395] 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 563a900f8f..819358aad2 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 0000000000..73257a3f48 --- /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 0000000000..62d59e258e --- /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 0000000000..82e0564fc6 --- /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 0000000000..2df20a0ede --- /dev/null +++ b/tests/harness/corpus-costs/7322-union-scalar-equality.ts.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 2800 } From 279cdcffae822a41934914bd1f2f71ff4966659f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:39:07 -0500 Subject: [PATCH 170/395] Name source files relative to the entry's directory in structural positions Structural positions started with a file's absolute path, so lowered names (closures, recursive records and unions, and every name keyed by a type that holds one) changed with the directory the program was compiled in. Union arms are ordered by their types' keys, so a program compiled in a new temporary directory could also get its union arms in another order. union-retype.test.ts compiles each program in a fresh temporary directory, and in about one run in ten the arms of the visitor's unions came out in an order where no retag keeps its tags, which failed "lowered identity retags share the box". Positions now name a file by its path relative to the entry's directory (default library files keep their base name), so the same program gets the same names wherever it sits. --- packages/compiler/src/frontend/lowering/lowerer.ts | 10 ++++++---- packages/compiler/test/stable-names.test.ts | 7 +++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 7a2bfd9e7c..747e396af3 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -181,7 +181,7 @@ export { newFnCtx, type FnCtx } from "./function-context.js"; * - Lexical scoping is resolved here: locals get function-unique ids * ("x.0", "x.1" for shadowing); the IR is scope-flat. */ -import { dirname, resolve } from "node:path"; +import { dirname, relative, resolve } from "node:path"; import { bindingSource } from "../binding-source.js"; import { tsgoPath } from "../dts-paths.js"; import * as ts from "../ts7/adapter.js"; @@ -3192,12 +3192,14 @@ export class Lowerer { errorPropertyDispatchRevision: string | null = null; /** Structural source positions (structural-positions.ts) for names that * stay the same when unrelated code moves. */ - /** Default library files are named by their base name: two checkers - * install them in different directories. */ + /** Files are named by their path relative to the entry's directory, so + * names do not depend on where the program sits, and default library + * files by their base name: two checkers install them in different + * directories. */ readonly positions = new StructuralPositions((sf) => this.program.isSourceFileDefaultLibrary(sf) ? `lib:${sf.fileName.slice(sf.fileName.lastIndexOf("/") + 1)}` - : sf.fileName, + : relative(dirname(this.entry.fileName), sf.fileName), ); /** The outermost node whose token span is exactly `loc`, or null. */ diff --git a/packages/compiler/test/stable-names.test.ts b/packages/compiler/test/stable-names.test.ts index 720472556d..c82db039d5 100644 --- a/packages/compiler/test/stable-names.test.ts +++ b/packages/compiler/test/stable-names.test.ts @@ -110,3 +110,10 @@ console.log(box.value); expect(locals).toHaveLength(2); expect(locals[0]!.name).not.toBe(locals[1]!.name); }); + +test("names do not depend on the directory the program sits in", async () => { + // Each call lowers in its own temporary directory. + const [first] = await lower(recursive); + const [second] = await lower(recursive); + expect(names(second!)).toEqual(names(first!)); +}); From 2d6ef06fcb04bcf46a1b0f789f86efb0dde5b649 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:40:22 -0500 Subject: [PATCH 171/395] 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 5fffa1428b..9741cc547a 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 ace8f294ce..2d70e90148 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 c3cee3feb5..81f0e9eeeb 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 a22de3445d..4b35507563 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 c1cf66af44..e4e37ec4ff 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 0000000000..f5cfba0cc4 --- /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 0000000000..383a2437f9 --- /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 0000000000..204a01a83e --- /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 172/395] 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 8461cc71c8..2a514a5a0c 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 c1bab6680b..0fa62e5585 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 630ef71733..a7a5e718a0 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 2a3f11be91..abbd023d48 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 c1cf66af44..4cfa36b5b3 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 0570d0ea7b..f50ae95e32 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 9a65d9a242..46bfc279e6 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 6d6c6f99c1..4611b3bb8a 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 703e8fe48f..3a41d91769 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 c884f11324..af61e52290 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 3a0b7870a5..d095099114 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 fd4420b698..995cb177aa 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 d8ccc15b7b..682b9bde61 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 6540b3a99a..20f1561241 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 16db45b6c5..6ddee15976 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 fcdbe86947..e561c0dbf2 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 5b984f318d..555f5d3063 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 bb36c70074..68bc61b219 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 af8d9b7245..35a747d603 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 0000000000..f1dcb625fb --- /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 0000000000..00c6c7f0c3 --- /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 0000000000..0faf76b0f7 --- /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 0000000000..5374e215e1 --- /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 0000000000..67d2b759b6 --- /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 0000000000..d4c01fab9d --- /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 012b860130..631750cdf9 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 5b8f3563f1..0e81033ad7 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 24cd8093b4..a3ac43cc01 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 791c600c7c..fea020d164 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 cdd8156422..384e8d3371 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 be5ccb9107..691b6af8ed 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 8b8a72406c..c78a03e1ab 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 0000000000..7cfb82e49b --- /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 0000000000..355116fa7d --- /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 0000000000..2df298d6db --- /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 0000000000..0e41fe1c84 --- /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 0000000000..57e84aff24 --- /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 0000000000..5972f6684b --- /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 0000000000..d94320c7ec --- /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 0000000000..a74da86c8f --- /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 0000000000..83ccdb03d9 --- /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 0000000000..2fc75720e0 --- /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 0000000000..b6c0a39a01 --- /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 0000000000..7e459d9783 --- /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 176caa5161..c0176eb93b 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 8487742019e824472ad29a96135c2e3553147ca6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:48:41 -0500 Subject: [PATCH 173/395] Read the shared decoded value after an untyped listener, and skip discriminated unions - Once a 'message' listener without a typed entry has decoded the message, the listeners after it read that decoded value, so a typed listener sees what an earlier untyped listener changed, as in Node. - A union with a literal discriminator gets no typed message entry: its checked conversion also tests the discriminator, which the message readers do not. Corpus 7361 checks a typed listener after an untyped one that changes the message, and a listener on a discriminated union. --- packages/compiler/src/backend/llvm/dyn.ts | 8 ++-- ...7361-message-listeners-share-value.ts.json | 6 +++ packages/runtime/src/scr_events_emitter.c | 9 ++-- packages/runtime/src/scr_events_emitter.h | 4 +- .../7361-message-listeners-share-value.ts | 45 +++++++++++++++++++ ...7361-message-listeners-share-value.ts.json | 1 + 6 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7361-message-listeners-share-value.ts.json create mode 100644 tests/corpus/7361-message-listeners-share-value.ts create mode 100644 tests/harness/corpus-costs/7361-message-listeners-share-value.ts.json diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index ac01a9209e..5c9d8e60f0 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -4681,8 +4681,8 @@ export class LlDyn { /** Whether a message reader can build T: numbers, booleans and strings; * records of them with only declared fields, none of union type; and - * boxed unions of those arms and null or undefined, with at most one - * number, boolean and string arm. */ + * boxed unions of those arms and null or undefined without a literal + * discriminator, with at most one number, boolean and string arm. */ private messageReadable(t: IrType, seen: Set): boolean { switch (t.kind) { case "f64": @@ -4706,7 +4706,9 @@ export class LlDyn { } case "union": { const def = this.host.unionsById.get(t.unionId); - if (!def || this.host.nullableUnions.has(t.unionId)) return false; + // A discriminated union's checked conversion also tests the arm's + // literal discriminator, which the readers do not. + if (!def || def.discriminant || this.host.nullableUnions.has(t.unionId)) return false; const scalars = new Set(); for (const arm of def.arms) { if (arm.kind === "nullT" || arm.kind === "undefinedT") continue; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7361-message-listeners-share-value.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7361-message-listeners-share-value.ts.json new file mode 100644 index 0000000000..f6a78ee9a6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7361-message-listeners-share-value.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7361-message-listeners-share-value.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_events_emitter.c b/packages/runtime/src/scr_events_emitter.c index 5e03c0d6ce..93a724ecb1 100644 --- a/packages/runtime/src/scr_events_emitter.c +++ b/packages/runtime/src/scr_events_emitter.c @@ -1049,8 +1049,8 @@ SCR_COLD static void scr_ee_listener_mismatch(const ScrStr *name, bool typed_emi } /* A message emit (scr_emitter_emit_message): listeners with a typed message - * entry read the message; the others share one decoded argument, made when - * the first of them runs. */ + * entry read the message until a listener without one runs; that listener + * and every later one share one decoded argument. */ struct ScrEeMessage { const struct ScrMessage *message; ScrDyn *(*decode)(const struct ScrMessage *); @@ -1101,7 +1101,10 @@ static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template, continue; } } - if (message && e->flex_message && e->flex_message(e->flex_cb, message->message)) { + /* Once a listener has the decoded value, later listeners read that + * value: an earlier listener may have changed it, as in Node. */ + if (message && !message->decoded && e->flex_message && + e->flex_message(e->flex_cb, message->message)) { scr_ee_entry_unref(e); continue; } diff --git a/packages/runtime/src/scr_events_emitter.h b/packages/runtime/src/scr_events_emitter.h index 6e1ef536a5..29d4c397fe 100644 --- a/packages/runtime/src/scr_events_emitter.h +++ b/packages/runtime/src/scr_events_emitter.h @@ -156,7 +156,9 @@ bool scr_emitter_emit(ScrEmitter *em, ScrStr *name, ...); bool scr_emitter_emit_flex(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc); /* An untyped emit of one message: a listener registered from a function * box with a typed message entry (scr_dyn_func_set_message) reads the - * message itself; the other listeners get decode(message), made once. */ + * message itself while no listener has needed the decoded value; from the + * first listener without an entry on, every listener gets decode(message), + * made once. */ struct ScrMessage; bool scr_emitter_emit_message(ScrEmitter *em, ScrStr *name, const struct ScrMessage *message, ScrDyn *(*decode)(const struct ScrMessage *)); diff --git a/tests/corpus/7361-message-listeners-share-value.ts b/tests/corpus/7361-message-listeners-share-value.ts new file mode 100644 index 0000000000..2a4e287df7 --- /dev/null +++ b/tests/corpus/7361-message-listeners-share-value.ts @@ -0,0 +1,45 @@ +import { MessageChannel, type MessagePort } from "node:worker_threads"; + +// Listeners of one message share its value, as in Node: a typed listener +// that runs after an untyped one sees the changes the untyped one made, +// and a listener on a discriminated union reads the arm its discriminator +// names. +type Item = { id: number; name: string }; +type Shape = { kind: "circle"; size: number } | { kind: "square"; size: number; corner: string }; + +const lines: string[] = []; +let pending = 0; +const ports: MessagePort[] = []; +function finish(): void { + if (--pending > 0) return; + for (const line of lines) console.log(line); + for (const port of ports) port.close(); +} + +const shared = new MessageChannel(); +ports.push(shared.port1); +shared.port1.on("message", (value: Item) => { + lines.push(`typed first ${value.id} ${value.name}`); +}); +shared.port1.on("message", (value: any) => { + value.id = value.id * 10; + value.name = `${value.name}!`; +}); +shared.port1.on("message", (value: Item) => { + lines.push(`typed after ${value.id} ${value.name}`); + finish(); +}); +pending += 2; +shared.port2.postMessage({ id: 1, name: "a" }); +shared.port2.postMessage({ id: 2, name: "b" }); + +const shapes = new MessageChannel(); +ports.push(shapes.port1); +shapes.port1.on("message", (shape: Shape) => { + if (shape.kind === "circle") lines.push(`circle ${shape.size}`); + else lines.push(`square ${shape.size} ${shape.corner}`); + finish(); +}); +pending += 2; +shapes.port2.postMessage({ kind: "circle", size: 3 }); +shapes.port2.postMessage({ kind: "square", size: 4, corner: "round" }); diff --git a/tests/harness/corpus-costs/7361-message-listeners-share-value.ts.json b/tests/harness/corpus-costs/7361-message-listeners-share-value.ts.json new file mode 100644 index 0000000000..c428eb8f11 --- /dev/null +++ b/tests/harness/corpus-costs/7361-message-listeners-share-value.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 4600 } From ddf7eb8a2bc226e215a9795f51cf3ac747c57572 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:50:32 -0500 Subject: [PATCH 174/395] Check with tsc-ts 0.0.7 and batch contextual types and signatures tsc-ts 0.0.7 answers getContextualTypes and getResolvedSignatures, so NODE_BATCH_QUERIES is on: lowering asks the checker for the contextual types of identity-flow sites, array literals and conditionals, and the resolved signatures of calls, in batches instead of node by node. The release-age exclusions move to tsc-ts 0.0.7 and its platform packages, and add @scriptc/threads 0.4.1, which tsc-ts 0.0.7 depends on. --- packages/compiler/package.json | 2 +- .../src/frontend/ts7/semantic-checker.ts | 9 +- pnpm-lock.yaml | 138 +++++++++--------- pnpm-workspace.yaml | 17 ++- 4 files changed, 83 insertions(+), 83 deletions(-) diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 98d984d4fa..a283f821aa 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "@scriptc/runtime": "workspace:*", - "tsc-ts": "0.0.5", + "tsc-ts": "0.0.7", "typescript": "7.0.2", "typescript5": "npm:typescript@5.9.3" }, diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.ts b/packages/compiler/src/frontend/ts7/semantic-checker.ts index d1d42c6e10..e7e6b408dc 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.ts @@ -97,11 +97,10 @@ export interface SemanticCompletion { } /** Whether lowering asks the checker for contextual types and resolved - * signatures in batches (`getContextualTypes`, `getResolvedSignatures`). - * Off until scriptc depends on a tsc-ts release that answers them: the - * batch prefetches (checker.ts) then do nothing and lowering asks node by - * node. */ -export const NODE_BATCH_QUERIES = false; + * signatures in batches (`getContextualTypes`, `getResolvedSignatures`, + * answered from tsc-ts 0.0.7 on). Off, the batch prefetches (checker.ts) do + * nothing and lowering asks node by node. */ +export const NODE_BATCH_QUERIES = true; /** The pinned checker's request surface over native objects. Every query * converges on the same snapshot/project registries, including batch and diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2404c38859..589de2a51d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,7 +55,7 @@ importers: version: 5.9.3 vercel: specifier: 58.1.0 - version: 58.1.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2) + version: 58.1.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2) vitest: specifier: ^3.0.0 version: 3.2.7(patch_hash=03a418fae669c2cd9846dc3c49e6c1b8ee7047bbdcda9d4f5d5ee09eb525a1be)(@edge-runtime/vm@3.2.0)(@types/node@24.13.3)(tsx@4.23.0) @@ -110,8 +110,8 @@ importers: specifier: workspace:* version: link:../runtime tsc-ts: - specifier: 0.0.5 - version: 0.0.5 + specifier: 0.0.7 + version: 0.0.7 typescript: specifier: 7.0.2 version: 7.0.2 @@ -271,14 +271,14 @@ packages: effect: ^4.0.0 redis: '>=5.0.0 <7.0.0' - '@emnapi/core@2.0.0-alpha.3': - resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} + '@emnapi/core@1.11.3': + resolution: {integrity: sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==} - '@emnapi/runtime@2.0.0-alpha.3': - resolution: {integrity: sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} - '@emnapi/wasi-threads@2.0.1': - resolution: {integrity: sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==} + '@emnapi/wasi-threads@1.2.3': + resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} '@esbuild/aix-ppc64@0.27.0': resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} @@ -1360,8 +1360,8 @@ packages: cpu: [x64] os: [win32] - '@scriptc/threads@0.3.6': - resolution: {integrity: sha512-XZb4JhXM/ivhx4MMLxqLLF2mCI28nUK+VVKlCnELO83tacUuGlq7YCe/8LwvzO7fZwy/rCH4+genAYYEc6BF6Q==} + '@scriptc/threads@0.4.1': + resolution: {integrity: sha512-wKgEBiVc07ZIGNFGwJpV5JbTXJN1N42qG6/g7ipnwt3klsU0nuWH2ToKV9vYH+RQstF7+twq6zMCf2qhUloTHQ==} engines: {node: '>=24.0.0'} '@sinclair/typebox@0.25.24': @@ -1377,42 +1377,42 @@ packages: '@ts-morph/common@0.11.1': resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==} - '@tsc-ts/darwin-arm64@0.0.5': - resolution: {integrity: sha512-yP64b2VpAKWG92KepBqxs1P89wpwiheYwNMk4EmmhSl6aw/ImIBrdHYNjvao3sg6FHZCXTc+PB6Vukh6hyQiJQ==} + '@tsc-ts/darwin-arm64@0.0.7': + resolution: {integrity: sha512-nIaTJKBpG6n60xxRYfBlh02G6aSO2punIvj4qPy8/CXZu0Kw4ra/J3PYJtrtYMIh/eIGo4+f1Q01v5dFPPwZbw==} cpu: [arm64] os: [darwin] - '@tsc-ts/darwin-x64@0.0.5': - resolution: {integrity: sha512-CEeHOFfpx5H9V3WSq+V7XGU8tfc06nRN818auiRVwuwPHvpti9gHq0HYkvf/43PH1pIGcgf68sQYdIxJhrQDKQ==} + '@tsc-ts/darwin-x64@0.0.7': + resolution: {integrity: sha512-8ITDyF3FG4SYeS1cV4+oRpbbk0jVsfd23Ww138wbQymbOLYjWPafzj4bymxnIE5kEWjZPVqTFDbaYqgWwNStag==} cpu: [x64] os: [darwin] - '@tsc-ts/linux-arm64-gnu@0.0.5': - resolution: {integrity: sha512-qwybOqGDEa35eV9x/3WO81ExAUbUMY8288ATC83kUfYGmOAVStaO+JM2IdrsiI8hLWVC7/f0RFgZ+7dyWh9/nA==} + '@tsc-ts/linux-arm64-gnu@0.0.7': + resolution: {integrity: sha512-nXGjtbM3YrARWOb+nkDaFdhKGSzPf3ofl/gV4x8JnVQ8SgK4IWJ5J0u93sk3mTYgdLj/Gcobp/D3lZ6WIzjKvA==} cpu: [arm64] os: [linux] libc: [glibc] - '@tsc-ts/linux-arm64-musl@0.0.5': - resolution: {integrity: sha512-6oSw3NbQIrnxxCRuEe190MbMFoOrM+P+u1usascQMgSjsMcVRnJ/ztvTnRrCDtfPEHJ5EC8drgWIpcdrhGsB0w==} + '@tsc-ts/linux-arm64-musl@0.0.7': + resolution: {integrity: sha512-hSjpQsOhrLaDsbvEO5EH9nC3Vy0WAt3aB+GejytEh9fl4F3kT0BasbBP4wJqu6S5SG9upLW7ZClXiNVnGYDGlQ==} cpu: [arm64] os: [linux] libc: [musl] - '@tsc-ts/linux-x64-gnu@0.0.5': - resolution: {integrity: sha512-KrlBLUcG7M6p7lN+ke+kHVLqy7IiY/H0FxFZzq5slXanhrps/BfjdkUahsyREg7KVprr08xFuXqJTBtLEI4eow==} + '@tsc-ts/linux-x64-gnu@0.0.7': + resolution: {integrity: sha512-MCU2MFefQGG/++nepg09BtYnsN2mVzeYfIS6GW30ChxUt2nJUMkEvzbDim9czDevW4LRqHgfnwEt+hs2ud1SDQ==} cpu: [x64] os: [linux] libc: [glibc] - '@tsc-ts/linux-x64-musl@0.0.5': - resolution: {integrity: sha512-fr8yGzkocCcTYoE+NwC+/WGPNteQ2GmOPJTy/z9NcsLchnpSIZ1fzFll15UYPKQlwQdNRJXxKK3npVBtLRdHTg==} + '@tsc-ts/linux-x64-musl@0.0.7': + resolution: {integrity: sha512-abTZZkn74772MPABStuXWJ1GHI07nbEf4a3Nwl7NYI5mLFYxeLSbq6+cGh0BUJeBeMhFNsjtQ5/j5sGmsrJ2tg==} cpu: [x64] os: [linux] libc: [musl] - '@tsc-ts/win32-x64-msvc@0.0.5': - resolution: {integrity: sha512-SQwufJafzsiOBYncHOxYkyELWjLlBijcKB+xOs6hr6Lxxp5MMLoPtvDFNG88c7G5xv2TDb1XbvyxnjQltr6SHw==} + '@tsc-ts/win32-x64-msvc@0.0.7': + resolution: {integrity: sha512-EOhLTkuWYytJYklR5BI1jRtTpEIPh1FQJKD8R37qqlcnPrELlRO6Nc2ncDpdCoYbpN+KrewIVPB3xD4o3VGUaw==} cpu: [x64] os: [win32] @@ -2577,8 +2577,8 @@ packages: ts-toolbelt@6.15.5: resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} - tsc-ts@0.0.5: - resolution: {integrity: sha512-Eea+zNv7y696pqNZtXBd0AX7/6PXIhR7aaEb0EhgpHadbk7VEiURD94/NngeLpYWts2tPjetkpwFLJY0+6yOnQ==} + tsc-ts@0.0.7: + resolution: {integrity: sha512-KRWTXRUvwMXi4At6xOWPDa/0BreY3MR2tPHooEMNWLZUA2eLphNe7PJiHclPLLn9RCnHY4ffDTTeF5hqgyBvtA==} engines: {node: '>=20'} hasBin: true @@ -2840,18 +2840,18 @@ snapshots: - bufferutil - utf-8-validate - '@emnapi/core@2.0.0-alpha.3': + '@emnapi/core@1.11.3': dependencies: - '@emnapi/wasi-threads': 2.0.1 + '@emnapi/wasi-threads': 1.2.3 tslib: 2.8.1 optional: true - '@emnapi/runtime@2.0.0-alpha.3': + '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@2.0.1': + '@emnapi/wasi-threads@1.2.3': dependencies: tslib: 2.8.1 optional: true @@ -3090,10 +3090,10 @@ snapshots: '@napi-rs/keyring-win32-ia32-msvc': 1.2.0 '@napi-rs/keyring-win32-x64-msvc': 1.2.0 - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@emnapi/core': 2.0.0-alpha.3 - '@emnapi/runtime': 2.0.0-alpha.3 + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 '@tybys/wasm-util': 0.10.3 optional: true @@ -3159,9 +3159,9 @@ snapshots: '@oxc-transform/binding-openharmony-arm64@0.111.0': optional: true - '@oxc-transform/binding-wasm32-wasi@0.111.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + '@oxc-transform/binding-wasm32-wasi@0.111.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -3340,9 +3340,9 @@ snapshots: '@rolldown/binding-openharmony-arm64@1.0.0-rc.1': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -3439,7 +3439,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true - '@scriptc/threads@0.3.6': {} + '@scriptc/threads@0.4.1': {} '@sinclair/typebox@0.25.24': {} @@ -3454,25 +3454,25 @@ snapshots: mkdirp: 1.0.4 path-browserify: 1.0.1 - '@tsc-ts/darwin-arm64@0.0.5': + '@tsc-ts/darwin-arm64@0.0.7': optional: true - '@tsc-ts/darwin-x64@0.0.5': + '@tsc-ts/darwin-x64@0.0.7': optional: true - '@tsc-ts/linux-arm64-gnu@0.0.5': + '@tsc-ts/linux-arm64-gnu@0.0.7': optional: true - '@tsc-ts/linux-arm64-musl@0.0.5': + '@tsc-ts/linux-arm64-musl@0.0.7': optional: true - '@tsc-ts/linux-x64-gnu@0.0.5': + '@tsc-ts/linux-x64-gnu@0.0.7': optional: true - '@tsc-ts/linux-x64-musl@0.0.5': + '@tsc-ts/linux-x64-musl@0.0.7': optional: true - '@tsc-ts/win32-x64-msvc@0.0.5': + '@tsc-ts/win32-x64-msvc@0.0.7': optional: true '@tybys/wasm-util@0.10.3': @@ -3563,7 +3563,7 @@ snapshots: '@typescript/typescript-win32-x64@7.0.2': optional: true - '@vercel/backends@0.8.27(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2)': + '@vercel/backends@0.8.27(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2)': dependencies: '@vercel/build-utils': 13.36.0 '@vercel/nft': 1.10.0(rollup@4.62.2) @@ -3571,10 +3571,10 @@ snapshots: execa: 3.2.0 fs-extra: 11.1.0 get-port: 5.1.1 - oxc-transform: 0.111.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + oxc-transform: 0.111.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) path-to-regexp: 8.3.0 resolve.exports: 2.0.3 - rolldown: 1.0.0-rc.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + rolldown: 1.0.0-rc.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) srvx: 0.11.16 ts-morph: 12.0.0 tsx: 4.21.0 @@ -3600,9 +3600,9 @@ snapshots: cjs-module-lexer: 1.2.3 es-module-lexer: 1.5.0 - '@vercel/cervel@0.1.35(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2)': + '@vercel/cervel@0.1.35(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2)': dependencies: - '@vercel/backends': 0.8.27(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2) + '@vercel/backends': 0.8.27(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -3638,9 +3638,9 @@ snapshots: '@vercel/error-utils@2.2.0': {} - '@vercel/express@0.1.118(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2)': + '@vercel/express@0.1.118(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2)': dependencies: - '@vercel/cervel': 0.1.35(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2) + '@vercel/cervel': 0.1.35(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2) '@vercel/nft': 1.10.0(rollup@4.62.2) '@vercel/node': 5.9.0(rollup@4.62.2) '@vercel/static-config': 3.4.0 @@ -4442,7 +4442,7 @@ snapshots: os-paths@4.4.0: {} - oxc-transform@0.111.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3): + oxc-transform@0.111.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3): optionalDependencies: '@oxc-transform/binding-android-arm-eabi': 0.111.0 '@oxc-transform/binding-android-arm64': 0.111.0 @@ -4460,7 +4460,7 @@ snapshots: '@oxc-transform/binding-linux-x64-gnu': 0.111.0 '@oxc-transform/binding-linux-x64-musl': 0.111.0 '@oxc-transform/binding-openharmony-arm64': 0.111.0 - '@oxc-transform/binding-wasm32-wasi': 0.111.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@oxc-transform/binding-wasm32-wasi': 0.111.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) '@oxc-transform/binding-win32-arm64-msvc': 0.111.0 '@oxc-transform/binding-win32-ia32-msvc': 0.111.0 '@oxc-transform/binding-win32-x64-msvc': 0.111.0 @@ -4604,7 +4604,7 @@ snapshots: reusify@1.1.0: {} - rolldown@1.0.0-rc.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3): + rolldown@1.0.0-rc.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3): dependencies: '@oxc-project/types': 0.110.0 '@rolldown/pluginutils': 1.0.0-rc.1 @@ -4619,7 +4619,7 @@ snapshots: '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.1 '@rolldown/binding-linux-x64-musl': 1.0.0-rc.1 '@rolldown/binding-openharmony-arm64': 1.0.0-rc.1 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.1(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.1 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.1 transitivePeerDependencies: @@ -4813,17 +4813,17 @@ snapshots: ts-toolbelt@6.15.5: {} - tsc-ts@0.0.5: + tsc-ts@0.0.7: dependencies: - '@scriptc/threads': 0.3.6 + '@scriptc/threads': 0.4.1 optionalDependencies: - '@tsc-ts/darwin-arm64': 0.0.5 - '@tsc-ts/darwin-x64': 0.0.5 - '@tsc-ts/linux-arm64-gnu': 0.0.5 - '@tsc-ts/linux-arm64-musl': 0.0.5 - '@tsc-ts/linux-x64-gnu': 0.0.5 - '@tsc-ts/linux-x64-musl': 0.0.5 - '@tsc-ts/win32-x64-msvc': 0.0.5 + '@tsc-ts/darwin-arm64': 0.0.7 + '@tsc-ts/darwin-x64': 0.0.7 + '@tsc-ts/linux-arm64-gnu': 0.0.7 + '@tsc-ts/linux-arm64-musl': 0.0.7 + '@tsc-ts/linux-x64-gnu': 0.0.7 + '@tsc-ts/linux-x64-musl': 0.0.7 + '@tsc-ts/win32-x64-msvc': 0.0.7 tslib@2.8.1: optional: true @@ -4894,9 +4894,9 @@ snapshots: dependencies: punycode: 2.3.1 - vercel@58.1.0(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2): + vercel@58.1.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2): dependencies: - '@vercel/backends': 0.8.27(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2) + '@vercel/backends': 0.8.27(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2) '@vercel/blob': 2.4.0 '@vercel/build-utils': 13.36.0 '@vercel/cli-auth': 0.3.1 @@ -4904,7 +4904,7 @@ snapshots: '@vercel/container': 0.1.0 '@vercel/detect-agent': 1.2.3 '@vercel/elysia': 0.1.104(rollup@4.62.2) - '@vercel/express': 0.1.118(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(rollup@4.62.2) + '@vercel/express': 0.1.118(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(rollup@4.62.2) '@vercel/fastify': 0.1.107(rollup@4.62.2) '@vercel/fun': 1.3.0 '@vercel/go': 3.10.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a3e992b0aa..ceb1506e9c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,14 +7,15 @@ minimumReleaseAgeExclude: - "effect@4.0.0" - "@effect/platform-node@4.0.0" - "@effect/platform-node-shared@4.0.0" - - "@tsc-ts/darwin-arm64@0.0.5" - - "@tsc-ts/darwin-x64@0.0.5" - - "@tsc-ts/linux-arm64-gnu@0.0.5" - - "@tsc-ts/linux-arm64-musl@0.0.5" - - "@tsc-ts/linux-x64-gnu@0.0.5" - - "@tsc-ts/linux-x64-musl@0.0.5" - - "@tsc-ts/win32-x64-msvc@0.0.5" - - "tsc-ts@0.0.5" + - "@tsc-ts/darwin-arm64@0.0.7" + - "@tsc-ts/darwin-x64@0.0.7" + - "@tsc-ts/linux-arm64-gnu@0.0.7" + - "@tsc-ts/linux-arm64-musl@0.0.7" + - "@tsc-ts/linux-x64-gnu@0.0.7" + - "@tsc-ts/linux-x64-musl@0.0.7" + - "@tsc-ts/win32-x64-msvc@0.0.7" + - "tsc-ts@0.0.7" + - "@scriptc/threads@0.4.1" allowBuilds: esbuild: true overrides: From 4e06e72b75fbc1d2095291cf38df48fc46515594 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:54:40 -0500 Subject: [PATCH 175/395] 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 3de1a4e6b1..02d0bc3e7d 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 83c9583deb..efeae4d8f8 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 cd2b1e2574ca8fde0f8516d7b8800a861246c312 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:55:01 -0500 Subject: [PATCH 176/395] Keep callback loop versions from nesting A callback loop version emits its loop twice. A loop inside either copy could be versioned again, so a nest of loops that each call a callback built just before them (`const g = adder(i)`) doubled at every level: a three-deep nest was emitted seven times per function. A loop inside a callback version, or the versioned loop itself emitted again for each copy, is no longer versioned, so a loop nest is emitted at most twice. The outer loop keeps its version; inner loops keep their calls as they are. --- .../src/backend/llvm/emit-statements.ts | 17 +++++----- .../test/callback-loop-versions.test.ts | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index ffd47c467a..4fe1a27bd4 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -1910,9 +1910,11 @@ export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }): /** The loop copies a callback loop version needs: the bindings the loop * calls that the constant-callback census resolves to one function, each * with that function's direct entry. Only small loops of optimized - * synchronous functions, and only bindings declared before the loop that - * an enclosing version has not already proved, qualify: the guard reads - * each binding before the loop runs. */ + * synchronous functions, and only bindings declared before the loop, + * qualify: the guard reads each binding before the loop runs. Versions do + * not nest: a loop inside either copy of a version (including the + * versioned loop itself, emitted again for each copy) keeps its calls as + * they are, so a loop nest is emitted at most twice. */ function callbackVersionBindings( emitter: LlEmitter, s: IrStmt & { kind: "for" | "while" | "doWhile" }, @@ -1920,7 +1922,7 @@ function callbackVersionBindings( if ( !emitter.callbackVersionsEnabled || emitter.currentConstantCallbacks.size === 0 || - emitter.callbackVersioning.has(s) + emitter.callbackVersioning.size > 0 ) return null; const found = new Map(); @@ -1930,12 +1932,7 @@ function callbackVersionBindings( const expr = (e: IrExpr): boolean => { if (++cost > MAX_CALLBACK_VERSION_COST) return false; if (e.kind === "assignExpr") bound.add(e.localId); - if ( - e.kind === "callValue" && - e.callee.kind === "varRef" && - !emitter.knownCallbacks.has(e.callee.localId) && - !found.has(e.callee.localId) - ) { + if (e.kind === "callValue" && e.callee.kind === "varRef" && !found.has(e.callee.localId)) { const local = emitter.currentLocals.get(e.callee.localId); const target = local && local.tdz !== true ? emitter.knownCallbackTarget(e.callee) : undefined; diff --git a/packages/compiler/test/callback-loop-versions.test.ts b/packages/compiler/test/callback-loop-versions.test.ts index c26abe1c4c..63a669fd44 100644 --- a/packages/compiler/test/callback-loop-versions.test.ts +++ b/packages/compiler/test/callback-loop-versions.test.ts @@ -81,3 +81,34 @@ test("a callback declared in the loop body is not read before the loop", async ( `); expect(llvm).not.toContain("callback.version"); }); + +test("loops inside a callback version are not versioned again", async () => { + // Each inner loop calls a callback a factory builds just before it; without + // the limit, every copy of a version would version the next loop again. + const llvm = await emitLlvm(` + function adder(k: number): (v: number) => number { + return (v: number) => v + k; + } + function nest(n: number, f: (v: number) => number): number { + let s = 0; + for (let i = 0; i < n; i++) { + s += f(i); + const g = adder(i); + for (let j = 0; j < n; j++) { + s += g(j); + const h = adder(j); + for (let k = 0; k < n; k++) s += h(k); + } + } + return s; + } + console.log(nest(3, (v) => v - 1), nest(2, (v) => v * 3)); + `); + // One version for the outer loop of each compiled `nest`; its two copies + // keep the inner loops whole. + const versions = [...llvm.matchAll(/^define [^\n]*@sc_bf_nest[^]*?^}/gm)].map( + ([body]) => body.match(/^callback\.version\.fast\d+:$/gm)?.length ?? 0, + ); + expect(versions.every((count) => count <= 1)).toBe(true); + expect(versions.some((count) => count === 1)).toBe(true); +}); From c9ae4d80196eeabdacc7574f3dd216bf0422f32e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:55:30 -0500 Subject: [PATCH 177/395] 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 387885c42c..df60fcb26c 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 e613c81f09..0ab001b8fb 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 b82c70d5de2853a368f13edb96ced5f14061fe2e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:56:19 -0500 Subject: [PATCH 178/395] Bound a follower's trip count only by a safe-integer span The trip count that bounds a follower's range was the driver's span over its step, computed in doubles. A span above 2^53 - 1 can round down: from -(2^53 - 1) to below 2^52 - 1 the span 3 * 2^52 - 3 becomes 3 * 2^52 - 4, so a follower that reaches 2^53 got the range limit 2^53 - 1 and ran as an unchecked integer. Such a span now bounds no trip count, and the follower is tested at run time (or the loop is refused when every value is known). Below it the quotient rounds to at least its floor, and the follower's products and sums are exact within the safe range. --- packages/compiler/src/ir/integer-loops.test.ts | 16 ++++++++++++++++ packages/compiler/src/ir/integer-loops.ts | 10 ++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/ir/integer-loops.test.ts b/packages/compiler/src/ir/integer-loops.test.ts index e1453abadd..b6ac27e3a2 100644 --- a/packages/compiler/src/ir/integer-loops.test.ts +++ b/packages/compiler/src/ir/integer-loops.test.ts @@ -525,4 +525,20 @@ test("counters stepped beside the tested one run as integers", () => { boxed.set("j", { ...vars.get("j")!, boxed: true }); expect(matchIntegerCountedForLoop(comoving(number(10)), boxed)).toBeNull(); expect(matchIntegerCountedForLoop(comoving(number(10), ref("i")), vars)).toBeNull(); + // A driver span above 2^53 - 1 bounds no trip count. From -(2^53 - 1) to + // below 2^52 - 1 the span 3 * 2^52 - 3 rounds down to 3 * 2^52 - 4, which + // would put this follower's last value at 2^53 - 1 instead of 2^53. + const wide = comoving(number(2 ** 52 - 1), number(-(2 ** 52) + 3), { + kind: "block", + body: [step("i", "+"), step("j", "+")], + loc, + }); + if (wide.init?.kind === "block" && wide.init.body[0]?.kind === "varDecl") + wide.init.body[0].init = number(-Number.MAX_SAFE_INTEGER); + expect(matchIntegerCountedForLoop(wide, vars)).toBeNull(); + if (wide.init?.kind === "block" && wide.init.body[1]?.kind === "varDecl") + wide.init.body[1].init = ref("from"); + expect(matchIntegerCountedForLoop(wide, vars)?.followers).toMatchObject([ + { startGuarded: true, tripGuarded: true }, + ]); }); diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index c4cdc327cd..c451c08527 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -269,10 +269,12 @@ export function matchIntegerCountedForLoop( }; // On body entry k the driver holds `start + step * k`, so k is at most the // driver's span over |step|, and a follower holds its start plus its own - // step times k. - const trips = Math.floor( - (step > 0 ? range.max - startRange.min : startRange.max - range.min) / Math.abs(step), - ); + // step times k. A span above 2^53 - 1 can round down, so it bounds no + // trip count. Below it the quotient rounds to at least its floor, and the + // follower's products and sums below are exact while they stay within + // ±(2^53 - 1) and round outside it otherwise. + const span = step > 0 ? range.max - startRange.min : startRange.max - range.min; + const trips = span <= Number.MAX_SAFE_INTEGER ? Math.floor(span / Math.abs(step)) : Infinity; const followers: IntegerLoopFollower[] = []; for (const id of counterIds) { if (id === local.id) continue; From 05acb4d24783f63cabf49cdc177d138fa5ea0f70 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:58:47 -0500 Subject: [PATCH 179/395] 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 aa8f175a35..f459f10479 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 180/395] 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 de16e5d611..8632bad47c 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 7ce8ae074a..4adfb87ab2 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 16db45b6c5..c2194a13c4 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 bb36c70074..163c2a41c4 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 ca579641a45bebcd629b2081b101233613375596 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:04:30 -0500 Subject: [PATCH 181/395] Index quoted get: and set: method names under their own keys --- .../lowering/class-dynamic-dispatch.ts | 13 +++++----- .../class-quoted-accessor-keys.mjs.json | 6 +++++ tests/corpus/class-quoted-accessor-keys.mjs | 26 +++++++++++++++++++ .../class-quoted-accessor-keys.mjs.json | 1 + 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-quoted-accessor-keys.mjs.json create mode 100644 tests/corpus/class-quoted-accessor-keys.mjs create mode 100644 tests/harness/corpus-costs/class-quoted-accessor-keys.mjs.json diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 79deb2aeb2..e792de957c 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -477,7 +477,7 @@ interface ReceiverIndex { /** By a method or generic method its base chain declares. */ methods: Map; /** By a method, generic method or accessor its base chain declares (an - * accessor under its property name), or a field it has. */ + * accessor under its key and under its property name), or a field it has. */ members: Map; rooted: number[]; } @@ -504,11 +504,12 @@ function receiverIndex(lowerer: Lowerer, plans: readonly PropertyReceiver[]): Re for (let c: ClassInfo | null = info; c; c = c.base) { for (const name of c.methods.keys()) { add(methods, name, position); - add( - members, - name.startsWith("get:") || name.startsWith("set:") ? name.slice(4) : name, - position, - ); + // A `get:`/`set:` key is an accessor of the property after the + // prefix, or a method whose quoted name has that prefix; the test + // looks up both. + add(members, name, position); + if (name.startsWith("get:") || name.startsWith("set:")) + add(members, name.slice(4), position); } if (c.genericMethods) for (const name of c.genericMethods.keys()) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-quoted-accessor-keys.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-quoted-accessor-keys.mjs.json new file mode 100644 index 0000000000..234634ebd6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-quoted-accessor-keys.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-quoted-accessor-keys.mjs" + ], + "diags": [] +} diff --git a/tests/corpus/class-quoted-accessor-keys.mjs b/tests/corpus/class-quoted-accessor-keys.mjs new file mode 100644 index 0000000000..637300b435 --- /dev/null +++ b/tests/corpus/class-quoted-accessor-keys.mjs @@ -0,0 +1,26 @@ +// Methods whose quoted names begin with "get:" or "set:", next to an +// accessor of the property after the prefix, read and called through +// untyped values. +class Quoted { + "get:size"() { + return "method get:size"; + } + "set:size"(value) { + return `method set:size ${value}`; + } + get size() { + return 3; + } +} +class Sized { + constructor() { + this.size = 5; + } +} +const values = [new Quoted(), new Sized(), { "get:size": () => "record get:size", size: 1 }]; +for (const value of values) { + const getter = value["get:size"]; + console.log(typeof getter, getter?.call(value)); + console.log(typeof value["set:size"], value.size); + if (value instanceof Quoted) console.log(value["get:size"](), value["set:size"](7)); +} diff --git a/tests/harness/corpus-costs/class-quoted-accessor-keys.mjs.json b/tests/harness/corpus-costs/class-quoted-accessor-keys.mjs.json new file mode 100644 index 0000000000..81da85b90d --- /dev/null +++ b/tests/harness/corpus-costs/class-quoted-accessor-keys.mjs.json @@ -0,0 +1 @@ +{ "plain": 3900, "san": 4000 } From e4dc0900d7f451963914422228bdebc18e4b1e20 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:09:08 -0500 Subject: [PATCH 182/395] 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 44d38acd46..5df3c7fe05 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 0000000000..8ed5027e54 --- /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 0000000000..3afc2cb33d --- /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 0000000000..7e94b56c64 --- /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 183/395] 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 ea7d7957bf..c1cca9c64d 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 906be0ccd7..fbfcade66d 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 c32a820c29..21fcbbd261 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 adeea728e1..5b451adfaa 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 20eee362fd..81165bd58c 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 7f8db4eb85..2c37484e07 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 40b55b0784..3ed562aea9 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 184/395] 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 29086d326b..cc01d7401e 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 4810c440e0..215b3f3c52 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 185/395] 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 b090298659..7af495d420 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 cdd00c4e7c..7a593e8652 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 0000000000..ae0d1f610b --- /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 0000000000..43fd59a6ec --- /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 0000000000..16148d8ce3 --- /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 186/395] 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 0000000000..171ab060a5 --- /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 df60fcb26c..c1d98b35b5 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 0000000000..cac0670e9d --- /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 0000000000..f9bfce7d9f --- /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 187/395] 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 df57813fe2..3934edbf02 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 c2449b4696..b75b5727da 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 188/395] 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 4b6ef6d903..b4f3957f5e 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 ed2f7b1765..d27ab62857 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 1c362cb09f..c971c672d4 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 2c7b8ea0a3..afd81d9b62 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 6387a0e4b3..94c60d836d 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 be27456163..40e77e6f13 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 35e866d6a4..78d451f465 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 53b22cfaa7..261879194b 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 08081ca1a2185a824a4fe8068447ec39e8e924f7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:22:22 -0500 Subject: [PATCH 189/395] Encode each field of a posted record completely before the next A typed record encoded from its field hooks queued a plain-object field on the visit stack and went on to the next field, so getters on later fields ran before the properties of earlier ones were read. Node's serializer finishes each field, nested properties included, before the next. The record path now drains the visit stack down to where it started after each field, so properties are read in Node's order. Corpus 7139 posts a record whose fields hold getter-bearing objects and prints the read order. --- packages/runtime/src/scr_message.c | 28 +++++++++++++++----- tests/corpus/7139-message-typed-records.ts | 30 ++++++++++++++++++++++ 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index 29cbb1be0e..d4eb263cff 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -331,6 +331,7 @@ static ScrDyn *scr_message_uncaught_keys(const ScrDyn *view) { static void scr_message_property(ScrMessageEncoder *encoder, size_t id, const ScrStr *key, size_t value, bool hidden); +static bool scr_message_drain(ScrMessageEncoder *encoder, size_t base); static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) { bool uncaught = encoder->uncaught; @@ -407,6 +408,9 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) void *rec = input->v.typed_ref.ptr; encoder->record_depth++; size_t result = id; + /* A field's value encodes completely, its containers' properties + * included, before the next field's: getters run in Node's order. */ + size_t base = encoder->depth; for (size_t i = 0; i < record->count; i++) { if (!record->fixed && !record->has(rec, i)) continue; ScrDyn *child = record->get(rec, i); @@ -422,6 +426,10 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) break; } scr_message_property(encoder, id, record->names[i], next, false); + if (encoder->depth > base && !scr_message_drain(encoder, base)) { + result = SIZE_MAX; + break; + } } encoder->record_depth--; return result; @@ -832,10 +840,11 @@ static ScrMessage *scr_message_pack(ScrMessageEncoder *encoder) { return message; } -static ScrMessage *scr_message_encode_run(ScrMessageEncoder *encoder, const ScrDyn *value) { - ScrMessage *message = NULL; - if (scr_message_value(encoder, value) == SIZE_MAX) goto done; - while (encoder->depth && !scr_exc_pending()) { +/* Encodes the properties of the containers on the visit stack above + * `base`, depth first, as Node's serializer visits them (getters run in + * that order). False when a property could not be read or encoded. */ +static bool scr_message_drain(ScrMessageEncoder *encoder, size_t base) { + while (encoder->depth > base && !scr_exc_pending()) { size_t id = encoder->stack[encoder->depth - 1]; ScrMessageVisit *visit = &encoder->visits[id]; if (visit->cursor == visit->keys->v.arr.len) { encoder->depth--; continue; } @@ -843,7 +852,7 @@ static ScrMessage *scr_message_encode_run(ScrMessageEncoder *encoder, const ScrD bool error = encoder->nodes[id].kind == SCR_MESSAGE_ERROR; if (key->kind != SCR_DYN_STR || (!error && !scr_dyn_property_is_enumerable(visit->view, key->v.str))) continue; ScrDyn *child = scr_dyn_bag_get(visit->view, key->v.str, visit->view); - if (!child || scr_exc_pending()) { scr_dyn_release(child); break; } + if (!child || scr_exc_pending()) { scr_dyn_release(child); return false; } bool hidden = error; if (visit->uncaught) { /* Node keeps an uncaught Error's data properties except functions @@ -854,10 +863,15 @@ static ScrMessage *scr_message_encode_run(ScrMessageEncoder *encoder, const ScrD } size_t next = scr_message_value(encoder, child); scr_dyn_release(child); - if (next == SIZE_MAX) break; + if (next == SIZE_MAX) return false; scr_message_property(encoder, id, key->v.str, next, hidden); } -done: + return !scr_exc_pending(); +} + +static ScrMessage *scr_message_encode_run(ScrMessageEncoder *encoder, const ScrDyn *value) { + ScrMessage *message = NULL; + if (scr_message_value(encoder, value) != SIZE_MAX) scr_message_drain(encoder, 0); if (!scr_exc_pending()) message = scr_message_pack(encoder); else for (size_t i = 0; i < encoder->count; i++) scr_message_drop(&encoder->nodes[i]); diff --git a/tests/corpus/7139-message-typed-records.ts b/tests/corpus/7139-message-typed-records.ts index 678b9cc483..35d0d15886 100644 --- a/tests/corpus/7139-message-typed-records.ts +++ b/tests/corpus/7139-message-typed-records.ts @@ -53,3 +53,33 @@ try { } catch (error) { console.log("rejected", (error as Error).name); } + +// Fields that hold objects with getters: each field's value is read +// completely, its getters included, before the next field's, as in Node. +type Holder = { id: number; first: unknown; second: unknown }; +const reads: string[] = []; +const getters: Holder = { + id: 1, + first: { + get a() { + reads.push("first.a"); + return 1; + }, + nested: { + get b() { + reads.push("first.nested.b"); + return 2; + }, + }, + }, + second: { + get c() { + reads.push("second.c"); + return 3; + }, + }, +}; +const { port1: getterPort, port2: getterSender } = new MessageChannel(); +getterSender.postMessage(getters); +console.log("getter order", reads.join(",")); +getterPort.close(); From ccfca67aecc819a66e38baafe01e213f99f1337f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:30:56 -0500 Subject: [PATCH 190/395] 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 b407132f47..354744e7b5 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 b650ca5672..301670c3a3 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 545c4d701c..8f7268daf9 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 065c963470..8365d4db93 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 e60660c09a..a48e1af781 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 0000000000..716cfe02fe --- /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 0000000000..f131361e8d --- /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 54baff8907..a3014fb9f0 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 8396f06937..08db077df2 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 8f26237179..2a4fff61be 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 0000000000..e829c5e333 --- /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 0000000000..5ffd9800b9 --- /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 0000000000..0812c90f18 --- /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 0000000000..5ffd9800b9 --- /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 191/395] 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 ace8f294ce..2e22832a14 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 c3cee3feb5..ad64260a29 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 bfde59a309..4933ba8b78 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 8970aa3bea..70e5266851 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 03a9fa2824..c5841006c0 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 0000000000..feda0ecf7c --- /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 0000000000..a8a975d862 --- /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 0000000000..793171eba1 --- /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 192/395] 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 75d584e4b6..668603d470 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 094b17f027..0a33b0e1ba 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 193/395] 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 5af1cad2f1..d3bd3c7f40 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 d61d91e5c8..41b64d4188 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 2d95e942cc..f921900a33 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 3073bd1bdc..8f179828d9 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 4488f90cbc..0ea0140596 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 3d9d410720..a87903f3a8 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 dc91da2fbc..d90a1deacb 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 bfde59a309..410db03717 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 8970aa3bea..4ddb5e75de 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 354004e9ed..02a9f971fe 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 6d5fe652d6..7d9be632df 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 cdd00c4e7c..223d455404 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 6de92f6670..22416fa8fa 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 0000000000..a89ae97b8b --- /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 c00d1b4f66..50b461614d 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 6c41b3dbd8..2fea1ef2c7 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 df4da1738f..ee0bb055a4 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 4be596ab5b..37ca28f168 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 675a13ca98..4ec9930553 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 0000000000..a50094f8ba --- /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 0000000000..8512b117c6 --- /dev/null +++ b/tests/harness/corpus-costs/4638-contract-program-lifetime-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 2800, "san": 4600 } From eef347115963cbe0723b0a53017b696b6f5f242e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:46:38 -0500 Subject: [PATCH 194/395] Stop asking batch methods the checker server answers as unknown tsgo has no getContextualTypes or getResolvedSignatures. Each prefetch asked for them anyway, and a failed chunk was split and retried down to single nodes, so a tsgo build paid about two failed requests per prefetched node. The checker facade now remembers a batch method the server answers with "unknown API method" and asks those queries node by node from then on. --- packages/compiler/src/frontend/ts7/checker.ts | 40 +++++++++++++++++-- .../src/frontend/ts7/semantic-checker.ts | 3 +- packages/compiler/test/ts7/facade.test.ts | 31 ++++++++++++++ 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index cd43636afc..9550d1dc22 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -86,12 +86,19 @@ function batchAnswers( items: readonly I[], fetch: (chunk: readonly I[]) => readonly (O | undefined)[], store: (item: I, answer: O | undefined) => void, -): void { + unknown: (error: unknown) => boolean = () => false, +): boolean { + let known = true; const answer = (chunk: readonly I[]): void => { + if (!known) return; let answers: readonly (O | undefined)[]; try { answers = fetch(chunk); - } catch { + } catch (error) { + if (unknown(error)) { + known = false; + return; + } if (chunk.length === 1) return; const mid = chunk.length >> 1; answer(chunk.slice(0, mid)); @@ -101,6 +108,13 @@ function batchAnswers( chunk.forEach((item, index) => store(item, answers[index])); }; for (let i = 0; i < items.length; i += BATCH_CHUNK) answer(items.slice(i, i + BATCH_CHUNK)); + return known; +} + +/** Whether a request failed because the server has no such method: tsgo, + * and tsc-ts before 0.0.7, answer the batch methods this way. */ +function unknownMethod(error: unknown, method: string): boolean { + return error instanceof Error && error.message.includes(`unknown API method "${method}"`); } function chunked(items: readonly T[], fetch: (chunk: readonly T[]) => readonly R[]): R[] { @@ -239,6 +253,9 @@ function collectNodes(roots: readonly Node[], walk: PrefetchWalk = "all"): Node[ export class CheckerFacade { private readonly cache = new CheckerCache(); + /** Batch methods the server answered as unknown. Lowering asks those + * node by node from then on, without a failed request per prefetch. */ + private readonly unknownBatchMethods = new Set(); constructor( /** The underlying 7.0.2 sync checker — exposed for methods the facade @@ -374,7 +391,8 @@ export class CheckerFacade { if (!NODE_BATCH_QUERIES) return; this.ensureActive(); const missing = [...new Set(nodes)].filter((node) => !this.cache.contextualType.has(node)); - batchAnswers( + this.batchNodes( + "getContextualTypes", missing, (chunk) => this.raw.getContextualTypes(chunk), (node, type) => this.cache.contextualType.set(node, type), @@ -386,13 +404,27 @@ export class CheckerFacade { if (!NODE_BATCH_QUERIES) return; this.ensureActive(); const missing = [...new Set(nodes)].filter((node) => !this.cache.resolvedSignature.has(node)); - batchAnswers( + this.batchNodes( + "getResolvedSignatures", missing, (chunk) => this.raw.getResolvedSignatures(chunk), (node, signature) => this.cache.resolvedSignature.set(node, signature), ); } + /** Answers a node batch method's chunks (batchAnswers), unless the server + * has answered the method as unknown. */ + private batchNodes( + method: string, + nodes: readonly Node[], + fetch: (chunk: readonly Node[]) => readonly (O | undefined)[], + store: (node: Node, answer: O | undefined) => void, + ): void { + if (nodes.length === 0 || this.unknownBatchMethods.has(method)) return; + if (!batchAnswers(nodes, fetch, store, (error) => unknownMethod(error, method))) + this.unknownBatchMethods.add(method); + } + /** Batches symbol queries for every identifier under roots without the * usual companion getTypeOfSymbol batch. Preflight uses this for AST * analyses that themselves inspect deferred bodies for binding identity: diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.ts b/packages/compiler/src/frontend/ts7/semantic-checker.ts index e7e6b408dc..c19f935989 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.ts @@ -99,7 +99,8 @@ export interface SemanticCompletion { /** Whether lowering asks the checker for contextual types and resolved * signatures in batches (`getContextualTypes`, `getResolvedSignatures`, * answered from tsc-ts 0.0.7 on). Off, the batch prefetches (checker.ts) do - * nothing and lowering asks node by node. */ + * nothing and lowering asks node by node. A server without a batch method + * (tsgo) answers it as unknown once, and the facade stops asking. */ export const NODE_BATCH_QUERIES = true; /** The pinned checker's request surface over native objects. Every query diff --git a/packages/compiler/test/ts7/facade.test.ts b/packages/compiler/test/ts7/facade.test.ts index 94bab8b31c..8e03400a2e 100644 --- a/packages/compiler/test/ts7/facade.test.ts +++ b/packages/compiler/test/ts7/facade.test.ts @@ -835,6 +835,37 @@ export function f(input: number): number { expect(panics).toBe(warmPanics); }); +test("a batch method the server answers as unknown is asked once, then node by node", () => { + const w = buildTwoWorlds(RICH_TS, host); + worlds.push(w); + const sf = w.p7.getSourceFile(w.files[0]!)!; + const raw = w.p7.project.checker; + const requests: Record = {}; + // A server without the batch methods, as tsgo is. + const older = new Proxy(raw, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + if (prop === "getContextualTypes" || prop === "getResolvedSignatures") { + return () => { + requests[prop] = (requests[prop] ?? 0) + 1; + throw new Error(`unknown API method "${prop}"`); + }; + } + return typeof value === "function" ? value.bind(target) : value; + }, + }) as Checker; + const facade = new CheckerFacade(older); + + facade.prefetchRoots([sf]); + expect(requests).toEqual({ getContextualTypes: 1, getResolvedSignatures: 1 }); + facade.prefetchRoots(sf.statements.nodes); + facade.prefetchContextualTypes(collectNodes(w)); + expect(requests).toEqual({ getContextualTypes: 1, getResolvedSignatures: 1 }); + // The node-by-node queries still answer. + const call = collectNodes(w).find(ad.isCallExpression)!; + expect(facade.getResolvedSignature(call)).toBe(raw.getResolvedSignature(call)); +}); + test("autoPrefetch: false degrades to per-call queries (the escape hatch works)", () => { const w = buildTwoWorlds(RICH_TS, host); worlds.push(w); From a6a1eb2b95c7b2c4d1ea436526151aba44a110c1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:57:53 -0500 Subject: [PATCH 195/395] 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 2a514a5a0c..060a0bf957 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 a7a5e718a0..a1b6736309 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 abbd023d48..380f5d814b 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 24f2bf0190..fd5b5d681d 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 46bfc279e6..94318c3aad 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 4611b3bb8a..5fd4af1a98 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 3a41d91769..a616dbec20 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 af61e52290..c2536ede81 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 d095099114..33b1ca38ec 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 39b7207c03..875fb9e7a4 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 213abfec6a..305cbd9b49 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 20f1561241..214a2402e3 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 6ddee15976..bab07c458c 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 e561c0dbf2..1708ad01a9 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 555f5d3063..da9cab1c3f 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 68bc61b219..a767840838 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 35a747d603..71b9570b45 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 0000000000..d29eed1237 --- /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 0000000000..e12ce5a523 --- /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 0000000000..07e468aa21 --- /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 0000000000..036d9836e5 --- /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 0000000000..0b5993d320 --- /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 0000000000..79354d9ce1 --- /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 0000000000..313c9f19e9 --- /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 0000000000..6fc082ea1b --- /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 631750cdf9..3d22b2360f 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 0e81033ad7..909412bc7a 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 a3ac43cc01..9bd5abf8c0 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 fea020d164..35332d0ca4 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 691b6af8ed..75debe6155 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 c78a03e1ab..0e0416e05a 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 8fd2201680..1db90bc5a8 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 0000000000..4887480216 --- /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 0000000000..14556348a8 --- /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 0000000000..4a2ba71893 --- /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 0000000000..ef6542eceb --- /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 0000000000..a9622173e1 --- /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 0000000000..0996e06eb9 --- /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 0000000000..b22b68dcef --- /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 0000000000..a4a3ce396d --- /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 0000000000..47f7b9ed9f --- /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 0000000000..7743ca7c88 --- /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 0000000000..19d896024a --- /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 0000000000..e8e0ab3d11 --- /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 0000000000..fe1307a1f9 --- /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 0000000000..ebaa9214cd --- /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 0000000000..15ff27f218 --- /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 0000000000..6bda6d8187 --- /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 c0176eb93b..c4013d2889 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 196/395] 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 b642041227..398384e757 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 56b65a3d30..3b64d65cee 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 c35860d435..4eef5dc545 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 89a5ee73ad..a9f5dda24a 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 daafba23b3..5d754bced5 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 b1afd8761c..d0adef3da7 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 ba3e270eed..9d76a548c8 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 0b945a1c9c..2b1aad28db 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 197/395] 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 afd81d9b62..18bfb32e69 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 94c60d836d..b13b674983 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 198/395] 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 e3cdfd077a..be8c948ac1 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 0000000000..10c1fbc8ab --- /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 b70023c67f..71306f5d12 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 89c32d6b7b..6533301f87 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 05d72c4be3..4d57644af3 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 ba340df40a..4b8764d804 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 08d89e8505..8bdefdf0e6 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 65a68470cf0e5dba6e51e473faebeaf9f0f2e7b0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:12:23 -0500 Subject: [PATCH 199/395] Free a typed teardown's blocks in the order it met them A typed teardown freed each block onto its free list as it went, and the free lists, which are LIFO, handed a torn-down structure's blocks to the next allocations in the reverse order. A structure that a loop rebuilds the same way each time was therefore laid out mirrored every other generation, and every walk over it, its teardown included, ran through memory backwards, which the hardware prefetchers handle three to four times worse. A program that rebuilds a tree of 2^21 - 1 object literals 12 times, summing each generation before dropping it, took 26 to 28 ns per node to build, 24 to sum and 16 to drop in those generations, against 6.8, 6.3 and 5.3 in the others. With this change, every generation runs at the second set of times, and the program takes 0.50 s instead of 1.18 s (Node: 0.91 s). The teardown of a class or record type with children of its own type now keeps the blocks it frees in a segment, linked through their first words and built in order through a tail pointer. A node releases its own block first, places it ahead of its children's when its address is the lower (object literals allocate before their fields) and behind them otherwise (constructor arguments build the children first), and the chain's first teardown frees the whole segment onto the free list in one step. A teardown past DIRECT_DESTROY_CHAIN appends through scr_rc_destroy_seg, which keeps scr_rc_destroy's depth budget and queue. Blocks outside the calling thread's arena are freed as before, and targets without the inline allocation paths keep the old teardown. --- packages/compiler/src/backend/llvm/alloc.ts | 77 +++++- .../compiler/src/backend/llvm/classes.test.ts | 59 +++++ packages/compiler/src/backend/llvm/classes.ts | 73 ++++-- packages/compiler/src/backend/llvm/shapes.ts | 238 ++++++++++++++++-- .../corpus/7260-teardown-segments.ts.json | 6 + packages/runtime/src/scr_cycle.c | 56 +++-- packages/runtime/src/scr_rc.h | 8 + scripts/ci-test-costs.json | 2 + tests/corpus/7260-teardown-segments.ts | 179 +++++++++++++ .../7260-teardown-segments.ts.json | 1 + tests/harness/teardown-segments.test.ts | 97 +++++++ 11 files changed, 730 insertions(+), 66 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7260-teardown-segments.ts.json create mode 100644 tests/corpus/7260-teardown-segments.ts create mode 100644 tests/harness/corpus-costs/7260-teardown-segments.ts.json create mode 100644 tests/harness/teardown-segments.test.ts diff --git a/packages/compiler/src/backend/llvm/alloc.ts b/packages/compiler/src/backend/llvm/alloc.ts index aa6345dabf..2efeb96acb 100644 --- a/packages/compiler/src/backend/llvm/alloc.ts +++ b/packages/compiler/src/backend/llvm/alloc.ts @@ -459,18 +459,72 @@ export type FreeObservers = "weak" | "weak-or-source"; * type's disposals), and the block — `scr_cyc_free` for collector-headered * objects, `scr_rt_free` otherwise. Labels use the `sf.` prefix; the lines * end in an open block. + * + * With `keep` (an alloca of ptr holding NULL; inline allocation paths only), + * a block that would go onto the calling thread's free list is stored there + * instead, for a teardown segment (emitSegmentFree); a block the system or + * another arena owns is still freed. */ export function emitObjectFree( host: AllocHost, cyc: boolean, observers: FreeObservers = "weak", + keep: string | null = null, ): string[] { - const lines = threadLocalState(host, "sf", freeLines(host, cyc, observers)); + const lines = threadLocalState(host, "sf", freeLines(host, cyc, observers, keep)); if (host.objectAudit === false) return lines.filter((line) => line !== FREE_NOTE); host.declare(`declare void @scr_obj_free_note()`); return lines; } +/** Lines that put the blocks `first` to `last` (linked through their first + * words; one block when they are equal) of the class at reservation offset + * `off` on the calling thread's free list. */ +function freeListPushLines(prefix: string, off: string, first: string, last: string): string[] { + return [ + ` %${prefix}.shp = getelementptr inbounds i8, ptr @scr_sa, i64 ${SA_SHIFT}`, + ` %${prefix}.sh32 = load i32, ptr %${prefix}.shp`, + ` %${prefix}.sh = zext i32 %${prefix}.sh32 to i64`, + ` %${prefix}.c = lshr i64 ${off}, %${prefix}.sh`, + ` %${prefix}.c8 = shl i64 %${prefix}.c, 3`, + ` %${prefix}.fo = add i64 %${prefix}.c8, ${SA_FREE}`, + ` %${prefix}.fp = getelementptr inbounds i8, ptr @scr_sa, i64 %${prefix}.fo`, + ` %${prefix}.head = load ptr, ptr %${prefix}.fp`, + ` store ptr %${prefix}.head, ptr ${last}`, + ` store ptr ${first}, ptr %${prefix}.fp`, + ]; +} + +/** + * Lines that free a teardown segment: the blocks `first` to `last` (ptr + * values, both NULL for an empty segment), linked through their first words, + * which emitObjectFree kept. Every block of a segment is in the calling + * thread's arena and of one size class, so the segment goes onto that + * class's free list in one step, and the next allocations of the class take + * its blocks in segment order. Labels use `prefix`; the lines end in the + * open block `${prefix}.done`. + */ +export function emitSegmentFree( + host: AllocHost, + prefix: string, + first: string, + last: string, +): string[] { + declareState(host); + const lines = [ + ` %${prefix}.none = icmp eq ptr ${first}, null`, + ` br i1 %${prefix}.none, label %${prefix}.done, label %${prefix}.push`, + `${prefix}.push:`, + ` %${prefix}.base = load i64, ptr @scr_sa`, + ` %${prefix}.bi = ptrtoint ptr ${first} to i64`, + ` %${prefix}.off = sub i64 %${prefix}.bi, %${prefix}.base`, + ...freeListPushLines(prefix, `%${prefix}.off`, first, last), + ` br label %${prefix}.done`, + `${prefix}.done:`, + ]; + return threadLocalState(host, prefix, lines); +} + /** Lines that leave in `%.obs` whether the program can observe a * disposal of this type (folds to a constant). */ function observedLines(prefix: string, observers: FreeObservers): string[] { @@ -482,8 +536,14 @@ function observedLines(prefix: string, observers: FreeObservers): string[] { ]; } -function freeLines(host: AllocHost, cyc: boolean, observers: FreeObservers): string[] { +function freeLines( + host: AllocHost, + cyc: boolean, + observers: FreeObservers, + keep: string | null, +): string[] { if (host.inlineAlloc !== true) { + if (keep !== null) throw new Error("a kept block needs the inline allocation paths"); if (cyc) { host.declare(`declare void @scr_cyc_free(ptr)`); return [` call void @scr_obj_free_note()`, ` call void @scr_cyc_free(ptr %o)`]; @@ -548,16 +608,9 @@ function freeLines(host: AllocHost, cyc: boolean, observers: FreeObservers): str ` %sf.inx = call i1 @llvm.expect.i1(i1 %sf.in, i1 true)`, ` br i1 %sf.inx, label %sf.push, label %sf.sys`, `sf.push:`, - ` %sf.shp = getelementptr inbounds i8, ptr @scr_sa, i64 ${SA_SHIFT}`, - ` %sf.sh32 = load i32, ptr %sf.shp`, - ` %sf.sh = zext i32 %sf.sh32 to i64`, - ` %sf.c = lshr i64 %sf.off, %sf.sh`, - ` %sf.c8 = shl i64 %sf.c, 3`, - ` %sf.fo = add i64 %sf.c8, ${SA_FREE}`, - ` %sf.fp = getelementptr inbounds i8, ptr @scr_sa, i64 %sf.fo`, - ` %sf.head = load ptr, ptr %sf.fp`, - ` store ptr %sf.head, ptr ${blk}`, - ` store ptr ${blk}, ptr %sf.fp`, + ...(keep !== null + ? [` store ptr ${blk}, ptr ${keep}`] + : freeListPushLines("sf", "%sf.off", blk, blk)), ` br label %sf.done`, `sf.sys: ; outside the reservation, or the allocator is compiled out`, ` call void @scr_obj_free_note()`, diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index 2d95e942cc..0c0b73aeb8 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -68,6 +68,65 @@ test("leaf layouts keep direct teardown while recursive layouts share the depth } }); +test("teardowns of types with children of their own type free their blocks as one segment", () => { + const linked = { kind: "object" as const, className: "Linked" }; + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }], + classes: [ + { + name: "Linked", + fields: [ + { name: "next", type: linked }, + { name: "other", type: linked }, + ], + methods: [], + loc, + }, + ], + records: [{ id: "branch", fields: [{ name: "next", type: linked }] }], + }; + const define = (llvm: string, name: string): string => + new RegExp(`^define internal [^@]*@${name}\\([^]*?^}`, "m").exec(llvm)?.[0] ?? ""; + const linkedDestroy = `${mangleClassRelease("Linked")}_destroy`; + const branchDestroy = `${mangleRecordRelease("branch")}_destroy`; + for (const workers of [false, true]) { + const llvm = emitLlvmModule({ ...mod, workers }, { pointerBits: 64 }); + // Linked's teardown keeps its block and its children's in a segment: it + // releases its own block first and places it by address against its + // first child, a child torn down directly appends to the segment, one + // past the direct chain appends through the runtime, and the chain's + // first teardown frees the segment onto the free list in one step. + const seg = define(llvm, `${linkedDestroy}_s`); + expect(seg.startsWith("define internal ptr"), "segment form").toBe(true); + expect(seg).toMatch( + /store ptr %(?:sf\.blk|o), ptr %sg\.own[^]*call ptr @sc_release_Linked_destroy_s\(/, + ); + // The children's releases have a copy for each place of the own block. + for (const copy of ["ta", "tb"]) + expect(seg).toContain( + `call ptr @scr_rc_destroy_seg(ptr %v0, ptr @${linkedDestroy}_sc, ptr @${linkedDestroy}, ptr %${copy}0.bt)`, + ); + expect(seg).not.toContain("call void @scr_rc_destroy("); + expect(seg).toContain("icmp ult ptr %sg.o, %sg.r1"); + const first = define(llvm, `${linkedDestroy}_d`); + expect(first).toContain(`call ptr @${linkedDestroy}_s(ptr %o, i32 %d, ptr %head)`); + expect(first).toContain("store ptr %first, ptr %sgf.fp"); + expect(first.includes("llvm.threadlocal.address"), "thread-local state").toBe(workers); + expect(define(llvm, linkedDestroy)).toContain(`call void @${linkedDestroy}_d(ptr %o, i32 0)`); + // A record whose child is another type frees its own block as before. + expect(define(llvm, `${branchDestroy}_s`)).toBe(""); + expect(define(llvm, branchDestroy)).toContain(`call void @${mangleClassRelease("Linked")}(`); + } + // Without the inline allocation paths (32-bit targets) a teardown frees + // through the runtime, block by block. + const llvm32 = emitLlvmModule(mod, { pointerBits: 32 }); + expect(define(llvm32, `${linkedDestroy}_s`)).toBe(""); + expect(define(llvm32, `${linkedDestroy}_d`)).toContain(`call void @${linkedDestroy}_d(`); +}); + test("releases split into an inlinable fast path and an out-of-line remainder", () => { // An array of the class itself makes the hierarchy cycle-capable. const kids = { kind: "array" as const, elem: { kind: "object" as const, className: "Node" } }; diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 3073bd1bdc..3d67fbfd3a 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -43,6 +43,9 @@ import { destroyDefinitions, directChildReleaseLines, directDestroyTarget, + segmentPrologueLines, + segmentTeardownLines, + segmentedTeardown, usedDirectTargets, type DirectDestroy, type RegionHooks, @@ -1078,25 +1081,62 @@ export function emitClassShapes( bounded && (directTargets.has(`object:${cls.name}`) || refFields.some((f) => directDestroyTarget(host, f.type, directTargets) !== null)); + // The teardown's own symbol, and whether it keeps a segment (shapes.ts + // segmentedTeardown). + const destroy = `${meta.hierarchy ? mangleClassReleaseDirect(cls.name) : mangleClassRelease(cls.name)}_destroy`; + const segmented = + chained && + !meta.compact && + runtimePrefix === null && + !isStreamRooted && + !meta.threadSlot && + segmentedTeardown( + host, + destroy, + refFields.map((f) => f.type), + directTargets, + ); // The field-releasing teardown body shared by both release shapes // (the public one on standalone classes, the DIRECT one on hierarchy // members). Runs at rc == 0. const teardown = (lines: string[]): void => { - refFields.forEach((f, t) => { - lines.push( - ` %f${t} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`, - ` %v${t} = load ptr, ptr %f${t}`, - ); + const release = (prefix: string, f: (typeof refFields)[number], t: number): string[] => { const direct = bounded ? directDestroyTarget(host, f.type, directTargets) : null; - if (direct) - lines.push( - ...directChildReleaseLines(host, `td${t}`, `%v${t}`, direct, llvmCommentText(f.name)), - ); - else - lines.push( + if (direct === null) + return [ ` call void ${releaseSym(host, f.type)}(ptr %v${t}) ; ${llvmCommentText(f.name)}`, - ); - }); + ]; + const own = segmented && direct.destroy === destroy; + return directChildReleaseLines( + host, + `${prefix}${t}`, + `%v${t}`, + direct, + llvmCommentText(f.name), + own, + ); + }; + const loads = refFields.map((f, t) => [ + ` %f${t} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`, + ` %v${t} = load ptr, ptr %f${t}`, + ]); + if (segmented) { + // A segmented teardown loads its fields, releases its own block, and + // then releases the fields (shapes.ts segmentTeardownLines). + const ownType = refFields.flatMap((f, t) => + directDestroyTarget(host, f.type, directTargets)?.destroy === destroy ? [`%v${t}`] : [], + ); + lines.push( + ...segmentPrologueLines(), + ...loads.flat(), + ...emitObjectFree(host, traced, "weak", "%sg.own"), + ...segmentTeardownLines(ownType, (prefix) => + refFields.flatMap((f, t) => release(prefix, f, t)), + ), + ); + return; + } + refFields.forEach((f, t) => lines.push(...loads[t]!, ...release("td", f, t))); if (runtimePrefix) lines.push(...regCall("td", runtimePrefix.drop, "")); if (isStreamRooted) lines.push(...stCall("tds", "scr_stream_st_release", "")); if (meta.threadSlot) lines.push(...slotForgetLines(host, struct, meta, "tdw")); @@ -1178,10 +1218,12 @@ export function emitClassShapes( ]; if (traced) reld.push(...cycleDeadLines(host, "teardown")); if (bounded) { - const destroy = `${mangleClassReleaseDirect(cls.name)}_destroy`; const destroyBody: string[] = []; teardown(destroyBody); - defs.push(...destroyDefinitions(destroy, destroyBody, chained), ``); + defs.push( + ...destroyDefinitions(destroy, destroyBody, chained, segmented ? host : null), + ``, + ); host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); reld.push(` call void @scr_rc_destroy(ptr %o, ptr @${destroy})`); } else teardown(reld); @@ -1203,6 +1245,7 @@ export function emitClassShapes( bounded, lifetime, chained, + segmented, ), ``, ); diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 6de92f6670..ae217a76cc 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -34,6 +34,7 @@ import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree, + emitSegmentFree, CYC_APPEND, cycleAppendDecls, cycleAppendHelper, @@ -1340,17 +1341,23 @@ export interface DirectDestroy { /** The release of child `value` (a loaded field, possibly NULL) inside a * teardown whose chain length is `%d`: the child's own release for a shared * or immortal reference, its teardown for the last one. Labels use `tag`; - * the lines end in the open block `${tag}.next`. */ + * the lines end in the open block `${tag}.next`. + * + * With `segment` (the child has the teardown's own type, in a segmented + * teardown; segmentPrologueLines), the child's teardown appends its blocks + * to the teardown's segment. */ export function directChildReleaseLines( host: ShapeHost, tag: string, value: string, target: DirectDestroy, comment: string, + segment = false, ): string[] { const S = host.sizeType; const C = target.count ?? S; - host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); + if (segment) host.declare(`declare ptr @scr_rc_destroy_seg(ptr, ptr, ptr, ptr)`); + else host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); const lines = [ ` %${tag}.nn = icmp ne ptr ${value}, null`, ` br i1 %${tag}.nn, label %${tag}.chk, label %${tag}.next`, @@ -1382,16 +1389,139 @@ export function directChildReleaseLines( ` br i1 %${tag}.short, label %${tag}.direct, label %${tag}.bounded`, `${tag}.direct:`, ` %${tag}.d = add i32 %d, 1`, - ` call void @${target.destroy}_d(ptr ${value}, i32 %${tag}.d)`, + ); + if (!segment) { + lines.push( + ` call void @${target.destroy}_d(ptr ${value}, i32 %${tag}.d)`, + ` br label %${tag}.next`, + `${tag}.bounded:`, + ` call void @scr_rc_destroy(ptr ${value}, ptr @${target.destroy})`, + ` br label %${tag}.next`, + `${tag}.next:`, + ); + return lines; + } + lines.push( + ` %${tag}.t = load ptr, ptr %sg.t`, + ` %${tag}.dt = call ptr @${target.destroy}_s(ptr ${value}, i32 %${tag}.d, ptr %${tag}.t)`, + ` store ptr %${tag}.dt, ptr %sg.t`, ` br label %${tag}.next`, `${tag}.bounded:`, - ` call void @scr_rc_destroy(ptr ${value}, ptr @${target.destroy})`, + ` %${tag}.bt = load ptr, ptr %sg.t`, + ` %${tag}.rt = call ptr @scr_rc_destroy_seg(ptr ${value}, ptr @${target.destroy}_sc, ptr @${target.destroy}, ptr %${tag}.bt)`, + ` store ptr %${tag}.rt, ptr %sg.t`, ` br label %${tag}.next`, `${tag}.next:`, ); return lines; } +/* Teardown segments. A typed teardown that frees each block onto its free + * list as it goes frees a tree in walk order, and the free lists, which are + * LIFO, hand the blocks to the next allocations in the reverse order: a tree + * built again in the same order as the last (each generation of a structure + * a loop rebuilds) is laid out mirrored, and every walk over it, the next + * teardown included, runs through memory backwards, which defeats the + * hardware prefetchers. The runtime frees large dead sets of the cycle + * collector in address order for the same reason (scr_mem_order_for_free). + * + * So the teardown of a type with children of its own type (the `_s` form, + * segmentedTeardown) keeps the blocks it frees in a segment, linked through + * their first words, which it builds in order through a tail pointer: the + * `_s` form takes the segment's last link (the first word of its last block, + * or the head slot of an empty segment) and returns the new one. A node + * first releases its own block (emitObjectFree, keeping it), then appends it + * ahead of its children's blocks when its address is the lower, behind them + * otherwise (segmentTeardownLines), and the children append theirs in field + * order, so a structure + * built parent first (object literals allocate before their fields) or + * children first (constructor arguments) comes back in ascending order. The + * chain's first teardown (`_d`) frees the whole segment onto the class's + * free list in one step (emitSegmentFree), and the walks over the next + * generation run forward. A teardown past DIRECT_DESTROY_CHAIN appends + * through scr_rc_destroy_seg, under the same depth budget as + * scr_rc_destroy. Blocks outside the calling thread's arena are freed as + * before; one type's blocks are all of one class. */ + +/** Whether the teardown of a type whose reference fields have `fieldTypes` + * (with direct targets `targets`) and whose own teardown is `destroy` keeps + * a segment: a child field has its own type, and the inline allocation paths + * are in use. */ +export function segmentedTeardown( + host: ShapeHost, + destroy: string, + fieldTypes: readonly IrType[], + targets: ReadonlyMap, +): boolean { + if (host.inlineAlloc !== true) return false; + return fieldTypes.some((t) => directDestroyTarget(host, t, targets)?.destroy === destroy); +} + +/** The first lines of a segmented teardown, in its entry block: the + * segment's last link (`%sg.t`, from the `%tail` argument) and the kept own + * block (`%sg.own`, for emitObjectFree). */ +export function segmentPrologueLines(): string[] { + return [ + ` %sg.t = alloca ptr`, + ` %sg.own = alloca ptr`, + ` store ptr %tail, ptr %sg.t`, + ` store ptr null, ptr %sg.own`, + ]; +} + +/** The rest of a segmented teardown after emitObjectFree kept (or freed) + * its own block: the block goes ahead of its children's when its address is + * below the first non-NULL one of `children` (loaded fields of the + * teardown's own type, which stand for their blocks) or there is none, and + * behind them otherwise; then the children's releases (`releases`, given a + * label prefix), and the segment's last link is returned. Each order has its + * own copy of the releases, so neither keeps the decision, or the block once + * it is appended, across the children's teardowns. */ +export function segmentTeardownLines( + children: readonly string[], + releases: (prefix: string) => string[], +): string[] { + const lines = [` %sg.o = load ptr, ptr %sg.own`, ` %sg.kept = icmp ne ptr %sg.o, null`]; + let ref = "null"; + [...children].reverse().forEach((child, i) => { + lines.push( + ` %sg.nn${i} = icmp ne ptr ${child}, null`, + ` %sg.r${i} = select i1 %sg.nn${i}, ptr ${child}, ptr ${ref}`, + ); + ref = `%sg.r${i}`; + }); + lines.push( + ` %sg.leaf = icmp eq ptr ${ref}, null`, + ` %sg.lower = icmp ult ptr %sg.o, ${ref}`, + ` %sg.first = or i1 %sg.leaf, %sg.lower`, + ` %sg.ahead = and i1 %sg.kept, %sg.first`, + ` br i1 %sg.ahead, label %sg.front, label %sg.back`, + `sg.front:`, + ...segmentAppendLines("a"), + ...releases("ta"), + ` br label %sg.ret`, + `sg.back:`, + ...releases("tb"), + ` %sg.keep = icmp ne ptr %sg.o, null`, + ` br i1 %sg.keep, label %sg.last, label %sg.ret`, + `sg.last:`, + ...segmentAppendLines("l"), + ` br label %sg.ret`, + `sg.ret:`, + ` %sg.tr = load ptr, ptr %sg.t`, + ` ret ptr %sg.tr`, + ); + return lines; +} + +function segmentAppendLines(tag: string): string[] { + return [ + ` %sg.t${tag} = load ptr, ptr %sg.t`, + ` store ptr %sg.o, ptr %sg.t${tag}`, + ` store ptr %sg.o, ptr %sg.t`, + ]; +} + /** The direct-teardown target of a reference of `type`, if `targets` (keyed * `record:` / `object:`) has one. Nullable unions hold the arm's * pointer, NULL or an immortal sentinel, which the child release handles. */ @@ -1434,12 +1564,42 @@ export function usedDirectTargets( /** The teardown definitions for `destroy`: with `chained`, the body takes * the chain length `%d` (directChildReleaseLines) and the runtime's - * one-argument entry starts a chain. */ + * one-argument entry starts a chain. With `segment` (the host, for a + * segmented teardown), the body is a segment teardown's + * (segmentPrologueLines to segmentTeardownLines), defined as `_s`, with + * `_sc` for scr_rc_destroy_seg, and `_d` starts a segment and frees it. */ export function destroyDefinitions( destroy: string, body: readonly string[], chained: boolean, + segment: ShapeHost | null = null, ): string[] { + if (segment !== null) + return [ + `define internal ptr @${destroy}_s(ptr %o, i32 %d, ptr %tail) ${FN_ATTRS} {`, + `entry:`, + ...body, + `}`, + `define internal ptr @${destroy}_sc(ptr %o, ptr %tail) ${FN_ATTRS} {`, + `entry:`, + ` %t = call ptr @${destroy}_s(ptr %o, i32 0, ptr %tail)`, + ` ret ptr %t`, + `}`, + `define internal void @${destroy}_d(ptr %o, i32 %d) ${FN_ATTRS} {`, + `entry:`, + ` %head = alloca ptr`, + ` store ptr null, ptr %head`, + ` %last = call ptr @${destroy}_s(ptr %o, i32 %d, ptr %head)`, + ` %first = load ptr, ptr %head`, + ...emitSegmentFree(segment, "sgf", "%first", "%last"), + ` ret void`, + `}`, + `define internal void @${destroy}(ptr %o) ${FN_ATTRS} {`, + `entry:`, + ` call void @${destroy}_d(ptr %o, i32 0)`, + ` ret void`, + `}`, + ]; if (!chained) return [ `define internal void @${destroy}(ptr %o) ${FN_ATTRS} {`, @@ -1478,12 +1638,13 @@ export function releaseBody( bounded = true, immortal = false, chained = false, + segmented = false, ): string[] { const S = host.sizeType; if (bounded) host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); const destroy = `${fnName}_destroy`; const lines = [ - ...(bounded ? destroyDefinitions(destroy, freeBody, chained) : []), + ...(bounded ? destroyDefinitions(destroy, freeBody, chained, segmented ? host : null) : []), ...releaseFastPath(host, fnName, `${fnName}_slow`, traced, comment, immortal), `define internal void @${fnName}_slow(ptr %o) noinline ${FN_ATTRS} {${comment ? ` ; ${comment}` : ""}`, `entry:`, @@ -1715,26 +1876,58 @@ export function emitRecordShapes( const chained = directTargets.has(`record:${shape.id}`) || refMembers.some((m) => directDestroyTarget(host, m.type, directTargets) !== null); + const destroy = `${mangleRecordRelease(shape.id)}_destroy`; + const bounded = refMembers.some((member) => needsBoundedRelease(member.type)); + const segmented = + bounded && + segmentedTeardown( + host, + destroy, + refMembers.map((m) => m.type), + directTargets, + ); + // Closed layouts can carry a kept record source (record-sources.ts). + const observers = recordMayKeepSource(shape) ? "weak-or-source" : "weak"; + // A segmented teardown loads its fields, releases its own block, and + // then releases the fields (segmentTeardownLines). + const loads: string[] = []; + const ownType: string[] = []; + const release = (prefix: string, m: (typeof refMembers)[number], i: number): string[] => { + const direct = directDestroyTarget(host, m.type, directTargets); + if (direct === null) + return [` call void ${releaseSym(host, m.type)}(ptr %v${i}) ; ${llvmCommentText(m.name)}`]; + const own = segmented && direct.destroy === destroy; + return directChildReleaseLines( + host, + `${prefix}${i}`, + `%v${i}`, + direct, + llvmCommentText(m.name), + own, + ); + }; for (const m of refMembers) { - freeBody.push( + const fieldLines = [ ` %f${t} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${m.index}`, ` %v${t} = load ptr, ptr %f${t}`, - ); - const direct = directDestroyTarget(host, m.type, directTargets); - if (direct) { - freeBody.push( - ...directChildReleaseLines(host, `td${t}`, `%v${t}`, direct, llvmCommentText(m.name)), - ); - } else { - freeBody.push( - ` call void ${releaseSym(host, m.type)}(ptr %v${t}) ; ${llvmCommentText(m.name)}`, - ); - } + ]; + if (segmented) { + loads.push(...fieldLines); + if (directDestroyTarget(host, m.type, directTargets)?.destroy === destroy) + ownType.push(`%v${t}`); + } else freeBody.push(...fieldLines, ...release("td", m, t)); t++; } - // Closed layouts can carry a kept record source (record-sources.ts). - const observers = recordMayKeepSource(shape) ? "weak-or-source" : "weak"; - freeBody.push(...emitObjectFree(host, traced, observers)); + if (segmented) + freeBody.push( + ...segmentPrologueLines(), + ...loads, + ...emitObjectFree(host, traced, observers, "%sg.own"), + ...segmentTeardownLines(ownType, (prefix) => + refMembers.flatMap((m, i) => release(prefix, m, i)), + ), + ); + else freeBody.push(...emitObjectFree(host, traced, observers)); defs.push( ...releaseBody( host, @@ -1742,9 +1935,10 @@ export function emitRecordShapes( traced, freeBody, "", - refMembers.some((member) => needsBoundedRelease(member.type)), + bounded, false, chained, + segmented, ), ``, ); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7260-teardown-segments.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7260-teardown-segments.ts.json new file mode 100644 index 0000000000..a7eac099f3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7260-teardown-segments.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7260-teardown-segments.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index 5e5029e149..1bd0ff7ac1 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -293,24 +293,22 @@ static SCR_TL ScrDestroyEntry *scr_destroy_pending; static SCR_TL size_t scr_destroy_count, scr_destroy_capacity; #endif -void scr_rc_destroy(void *obj, void (*destroy)(void *)) { - if (scr_destroy_depth == SCR_DESTROY_DEPTH) { - if (scr_destroy_count == scr_destroy_capacity) { - size_t capacity = scr_destroy_capacity ? scr_destroy_capacity * 2 : 64; - if (capacity < scr_destroy_capacity || capacity > SIZE_MAX / sizeof(ScrDestroyEntry)) - scr_cyc_oom(); - ScrDestroyEntry *entries = realloc(scr_destroy_pending, capacity * sizeof(*entries)); - if (!entries) scr_cyc_oom(); - scr_destroy_pending = entries; - scr_destroy_capacity = capacity; - } - scr_destroy_pending[scr_destroy_count++] = (ScrDestroyEntry){ obj, destroy }; - return; +/* Queues a disposal past the depth budget; the outermost disposal runs it. */ +static void scr_rc_destroy_defer(void *obj, void (*destroy)(void *)) { + if (scr_destroy_count == scr_destroy_capacity) { + size_t capacity = scr_destroy_capacity ? scr_destroy_capacity * 2 : 64; + if (capacity < scr_destroy_capacity || capacity > SIZE_MAX / sizeof(ScrDestroyEntry)) + scr_cyc_oom(); + ScrDestroyEntry *entries = realloc(scr_destroy_pending, capacity * sizeof(*entries)); + if (!entries) scr_cyc_oom(); + scr_destroy_pending = entries; + scr_destroy_capacity = capacity; } - scr_destroy_depth++; - destroy(obj); - scr_destroy_depth--; - if (scr_destroy_depth) return; + scr_destroy_pending[scr_destroy_count++] = (ScrDestroyEntry){ obj, destroy }; +} + +/* Runs the queued disposals once the outermost one has returned. */ +static void scr_rc_destroy_drain(void) { while (scr_destroy_count) { ScrDestroyEntry entry = scr_destroy_pending[--scr_destroy_count]; scr_destroy_depth = 1; @@ -326,6 +324,30 @@ void scr_rc_destroy(void *obj, void (*destroy)(void *)) { } } +void scr_rc_destroy(void *obj, void (*destroy)(void *)) { + if (scr_destroy_depth == SCR_DESTROY_DEPTH) { + scr_rc_destroy_defer(obj, destroy); + return; + } + scr_destroy_depth++; + destroy(obj); + scr_destroy_depth--; + if (scr_destroy_depth == 0) scr_rc_destroy_drain(); +} + +void *scr_rc_destroy_seg(void *obj, void *(*destroy_seg)(void *, void *), + void (*destroy)(void *), void *tail) { + if (scr_destroy_depth == SCR_DESTROY_DEPTH) { + scr_rc_destroy_defer(obj, destroy); + return tail; + } + scr_destroy_depth++; + tail = destroy_seg(obj, tail); + scr_destroy_depth--; + if (scr_destroy_depth == 0) scr_rc_destroy_drain(); + return tail; +} + /* Live cycle-headered objects and the running count of OLD objects freed. * The inline allocation/free paths (scr_runtime.h, llvm/alloc.ts) maintain * both counters so full passes can measure old-generation reclamation. */ diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 7a921b4511..83d0589c16 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -552,6 +552,14 @@ _Static_assert(sizeof(((ScrCycHdr *)0)->gen) == 1 && SCR_CYC_OLD == 2, * depth; deeper objects wait until the outer disposal drains the worklist. * The callback owns the object and must release its children and free it. */ void scr_rc_destroy(void *obj, void (*destroy)(void *)); +/* scr_rc_destroy for an emitted teardown that keeps the blocks it frees in + * a segment, linked through their first words, instead of freeing them onto + * the free list (llvm/shapes.ts): destroy_seg tears obj down, appending its + * blocks after the segment's last link `tail`, and returns the new last + * link, which this returns. A disposal past the depth budget is queued with + * `destroy`, which frees its own blocks, and leaves `tail` as it was. */ +void *scr_rc_destroy_seg(void *obj, void *(*destroy_seg)(void *, void *), + void (*destroy)(void *), void *tail); /* RC hooks for cycle-headered types. on_release: rc was decremented and * stayed above zero — buffer the object as a possible cycle root (may run diff --git a/scripts/ci-test-costs.json b/scripts/ci-test-costs.json index 4c49645796..99d47e972c 100644 --- a/scripts/ci-test-costs.json +++ b/scripts/ci-test-costs.json @@ -409,6 +409,7 @@ "tests/harness/stdlib-surface.test.ts": 48, "tests/harness/strip-vt-linear.test.ts": 1795, "tests/harness/surface-manifest.test.ts": 5309, + "tests/harness/teardown-segments.test.ts": 2600, "tests/harness/terminal-geometry.test.ts": 2730, "tests/harness/test262-runner.test.ts": 207, "tests/harness/three-library.test.ts": 251122, @@ -828,6 +829,7 @@ "tests/harness/stdlib-surface.test.ts": 46, "tests/harness/strip-vt-linear.test.ts": 2546, "tests/harness/surface-manifest.test.ts": 5951, + "tests/harness/teardown-segments.test.ts": 5, "tests/harness/terminal-geometry.test.ts": 3270, "tests/harness/test262-runner.test.ts": 230, "tests/harness/three-library.test.ts": 361470, diff --git a/tests/corpus/7260-teardown-segments.ts b/tests/corpus/7260-teardown-segments.ts new file mode 100644 index 0000000000..cbd2b144d5 --- /dev/null +++ b/tests/corpus/7260-teardown-segments.ts @@ -0,0 +1,179 @@ +// Teardowns that hand their blocks back in order. +// +// A typed teardown of a type with children of its own type keeps the blocks +// it frees in one segment and frees the segment when the chain's first +// teardown returns, so the next structure built of that type reuses them in +// order. This program rebuilds structures of such types for many +// generations and reads every value of each generation before dropping it: +// a block handed out twice, or one freed while still in use, shows up as a +// wrong checksum. The shapes cover the ways a segment is put together: +// record literals (allocated before their fields) and constructors whose +// arguments build the children first, lists built by prepending and by +// appending, trees far deeper than one direct teardown chain and lists +// longer than the runtime's whole depth budget (whose tails wait in its +// queue), subtrees still owned elsewhere when their parent goes, records of +// one type holding records of another, and nodes of a cycle-capable class +// that are still candidates of the cycle collector when the last reference +// goes, and rings of them that only the collector frees. + +interface Node { + left: Node | null; + right: Node | null; + value: number; +} + +function literalTree(depth: number, value: number): Node { + return depth === 0 + ? { left: null, right: null, value } + : { left: literalTree(depth - 1, value * 2), right: literalTree(depth - 1, value * 2 + 1), value }; +} + +function sum(node: Node | null): number { + return node === null ? 0 : node.value + sum(node.left) + sum(node.right); +} + +class Branch { + readonly left: Branch | null; + readonly right: Branch | null; + readonly label: string; + constructor(left: Branch | null, right: Branch | null, label: string) { + this.left = left; + this.right = right; + this.label = label; + } +} + +function classTree(depth: number, label: string): Branch { + return depth === 0 + ? new Branch(null, null, label) + : new Branch(classTree(depth - 1, label + "l"), classTree(depth - 1, label + "r"), label); +} + +function labels(branch: Branch | null): number { + return branch === null ? 0 : branch.label.length + labels(branch.left) + labels(branch.right); +} + +interface Cell { + next: Cell | null; + n: number; +} + +function prepended(count: number, base: number): Cell | null { + let head: Cell | null = null; + for (let i = 0; i < count; i++) head = { next: head, n: base + i }; + return head; +} + +function appended(count: number, base: number): Cell | null { + if (count === 0) return null; + const head: Cell = { next: null, n: base }; + let tail = head; + for (let i = 1; i < count; i++) { + const cell: Cell = { next: null, n: base + i }; + tail.next = cell; + tail = cell; + } + return head; +} + +function cells(head: Cell | null): [number, number] { + let count = 0; + let total = 0; + for (let cell = head; cell !== null; cell = cell.next) { + count++; + total += cell.n; + } + return [count, total]; +} + +// Generations of literal trees: shallow, one chain deep, and several chains +// deep. Each generation is checked against the sum the shape must have. +for (const depth of [0, 1, 5, 16, 17, 18]) { + let ok = true; + for (let generation = 0; generation < 6; generation++) { + const tree = literalTree(depth, 1); + const expected = ((2 ** (depth + 1)) * (2 ** (depth + 1) - 1)) / 2; + if (sum(tree) !== expected) ok = false; + } + console.log(`literal trees of depth ${depth}: ${ok}`); +} + +// Generations of class trees built children first. +for (const depth of [3, 12, 17]) { + let total = 0; + for (let generation = 0; generation < 4; generation++) total += labels(classTree(depth, "t")); + console.log(`class trees of depth ${depth}: ${total}`); +} + +// Lists both ways, alternating, so each reuses the other's blocks; the long +// ones outlast the runtime's depth budget. +for (const count of [1, 15, 16, 17, 1000, 3000, 40000]) { + const seen: string[] = []; + for (let generation = 0; generation < 4; generation++) { + const list = generation % 2 === 0 ? prepended(count, generation) : appended(count, generation); + const [length, total] = cells(list); + seen.push(`${length}/${total}`); + } + console.log(`lists of ${count}: ${seen.join(" ")}`); +} + +// Subtrees owned elsewhere survive their parent's teardown, and the blocks +// freed around them are not handed out while they live. +const kept: Node[] = []; +for (let generation = 0; generation < 5; generation++) { + const tree = literalTree(10, generation + 1); + if (tree.left !== null && tree.left.right !== null) kept.push(tree.left.right); + if (tree.right !== null) kept.push(tree.right); + // Building the next tree reuses the dropped blocks. + literalTree(10, 7); +} +console.log(`kept subtrees: ${kept.length} ${kept.map(sum).join(",")}`); + +// A record of one type holding trees of another: the outer teardown frees +// its own blocks as before and tears each inner tree down as a segment. +interface Forest { + first: Node; + second: Node | null; + rest: Forest | null; +} + +function forest(count: number): Forest | null { + let out: Forest | null = null; + for (let i = 0; i < count; i++) out = { first: literalTree(4, i), second: i % 3 === 0 ? null : literalTree(2, i), rest: out }; + return out; +} + +let forestTotal = 0; +for (let generation = 0; generation < 3; generation++) { + for (let f = forest(500); f !== null; f = f.rest) forestTotal += sum(f.first) + sum(f.second); +} +console.log(`forests: ${forestTotal}`); + +// A cycle-capable class: releases of shared links make nodes candidates of +// the cycle collector, which are then torn down while still buffered, and +// the peer links close some rings, which the collector frees. +class Ring { + next: Ring | null = null; + peer: Ring | null = null; + id: number; + constructor(id: number) { + this.id = id; + } +} + +let ringTotal = 0; +for (let generation = 0; generation < 3; generation++) { + let head: Ring | null = null; + const peers: Ring[] = []; + for (let i = 0; i < 2000; i++) { + const ring: Ring = new Ring(i); + ring.next = head; + if (i % 7 === 0) peers.push(ring); + head = ring; + } + for (let r = head; r !== null; r = r.next) if (r.id % 5 === 0) r.peer = peers[r.id % peers.length] ?? null; + for (const peer of peers) ringTotal += peer.id; + peers.length = 0; + for (let r = head; r !== null; r = r.next) ringTotal += r.peer === null ? 0 : r.peer.id; +} +console.log(`rings: ${ringTotal}`); diff --git a/tests/harness/corpus-costs/7260-teardown-segments.ts.json b/tests/harness/corpus-costs/7260-teardown-segments.ts.json new file mode 100644 index 0000000000..7f7ce1c2b3 --- /dev/null +++ b/tests/harness/corpus-costs/7260-teardown-segments.ts.json @@ -0,0 +1 @@ +{ "plain": 5343, "san": 11960 } diff --git a/tests/harness/teardown-segments.test.ts b/tests/harness/teardown-segments.test.ts new file mode 100644 index 0000000000..95a2d27950 --- /dev/null +++ b/tests/harness/teardown-segments.test.ts @@ -0,0 +1,97 @@ +/* Teardown segments (llvm/shapes.ts): a typed teardown of a type with + * children of its own type keeps the blocks it frees in a segment and frees + * the segment onto the free list when the chain's first teardown returns. + * Corpus program 7260 pins what such programs print; this suite checks the + * memory side natively, which Node parity cannot: a program that rebuilds + * large trees and lists for a hundred generations reuses every block of the + * last generation, so its resident set stays at about one generation's + * worth. A segment that lost blocks would grow it by a generation each time. + * The sanitized lane frees through the system allocator (no segments) and + * keeps freed memory in quarantine, so the bound is checked in the plain + * lane only. */ +import { execFile } from "node:child_process"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const exec = promisify(execFile); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +const PROGRAM = ` +interface Node { + left: Node | null; + right: Node | null; + value: number; +} +function tree(depth: number): Node { + return depth === 0 + ? { left: null, right: null, value: 1 } + : { left: tree(depth - 1), right: tree(depth - 1), value: depth }; +} +function size(node: Node | null): number { + return node === null ? 0 : 1 + size(node.left) + size(node.right); +} +class Branch { + readonly left: Branch | null; + readonly right: Branch | null; + constructor(left: Branch | null, right: Branch | null) { + this.left = left; + this.right = right; + } +} +function branches(depth: number): Branch { + return depth === 0 ? new Branch(null, null) : new Branch(branches(depth - 1), branches(depth - 1)); +} +function count(branch: Branch | null): number { + return branch === null ? 0 : 1 + count(branch.left) + count(branch.right); +} +interface Cell { + next: Cell | null; + n: number; +} +function cells(n: number): Cell | null { + let head: Cell | null = null; + for (let i = 0; i < n; i++) head = { next: head, n: i }; + return head; +} +let total = 0; +let settled = 0; +for (let generation = 0; generation < 100; generation++) { + total += size(tree(16)) + count(branches(15)); + for (let c = cells(100000); c !== null; c = c.next) total += 1; + if (generation === 2) settled = process.memoryUsage.rss(); +} +const growth = Math.round((process.memoryUsage.rss() - settled) / (1024 * 1024)); +console.log(total, growth); +`; + +test.skipIf(sanitize)( + "rebuilding large trees and lists reuses every block their teardowns free", + { timeout: 300_000 }, + async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-segments-")); + try { + const source = join(dir, "churn.ts"); + await writeFile(source, PROGRAM); + const result = await compile(source, { + backend: "llvm", + outDir: dir, + outPath: join(dir, "churn"), + }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + if (!result.ok) return; + const { stdout } = await exec(result.binaryPath, [], { timeout: 120_000 }); + const [total, growth] = stdout.trim().split(" ").map(Number); + // 100 generations of 131071 + 65535 nodes and 100000 cells. + expect(total).toBe(100 * (131071 + 65535 + 100000)); + // One generation is about 10 MiB; a lost generation per round would be + // hundreds. + expect(growth).toBeLessThan(8); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, +); From 9aaebbab254ac0ee6359e10eca248fefab0c3559 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:14:27 -0500 Subject: [PATCH 200/395] 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 31d34e8db6..2bdd47363d 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 482e5f1e5d..23584b60a8 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 b1ced2c031..d9e6dafc66 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 0aa213279c..5b8cb4db23 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 4ac5434ae7..0901602ec9 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 7473c36bc8..f01478aad2 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 08e048cd1c..a6986a66aa 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 54e3238396..6b03811b82 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 d66ba32478..034747c290 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 a283f821aa..a1d66963d1 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 45b3edbaa9..637a5f831b 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 5b6906de64..cfb4151a4f 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 472056c394..615fe9d819 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 7f55cb6b54..fd6caf9766 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 0e1a55b227..7d66257f4c 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 efdd97444b..b3a01089d2 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 39667d5358..0b3264ec75 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 9efd418451..6c83d1cb9b 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 329fa308c5..9fb682f0f6 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 75c54e9c32..362fbc572d 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 7473b05f3f..a4176ed604 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 ab8666c88a..91bc4182bc 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 a3a2546854..2fdd688973 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 7981422b87..cbdf96d1de 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 d633ac3031..e0d2289259 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 460ca25fad..70eee223fc 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 9c6336a046..5822fd21d9 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 e9f885a5c2..1db34d4a7b 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 c9a40c2c05..5bbd237551 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 f7e4c98602..df4a9a1096 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 1af249b9d6..9232266afd 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 2aeeb7cbb2..11b8ef6243 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 ffbbdf4a48..689c7b72fc 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 201/395] 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 c1d98b35b5..df75435009 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 cac0670e9d..97c93d835c 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 202/395] 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 2653ec51ae..e8acbfbb53 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 0000000000..fa27f4ea81 --- /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 0000000000..bbf2177013 --- /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 80e01b08c5..b0f30ac74c 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 0000000000..49e5c72548 --- /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 0000000000..77911aa2d6 --- /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 3d10183f48..a23395c915 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 0000000000..fe4f81a903 --- /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 3158de9972..160e33438c 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 203/395] 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 6aa1b2a285..afc645ab32 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 d7e0e92aae..2bad2a0ce3 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 5c9d8e60f0..5946348937 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 0000000000..a778aa8f09 --- /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 3d9d410720..75e6ad50f3 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 dc91da2fbc..e3998e7824 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 bfde59a309..1ca9313b71 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 b91cdc5437..398aebb21e 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 4bcf2a3d54..a4e7cde2c0 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 8970aa3bea..1d42c47309 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 354004e9ed..6f960c11f5 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 a0a0cf24a6..7cb0fa7723 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 0570d0ea7b..42c803697a 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 ce9c40e217..4d151fc7e9 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 184fdb99e9..e32fd238d9 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 791987f86f..321f1d0059 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 c6bc4d97bc..56e94461f0 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 2f45c3cd2b..60270adda3 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 6de92f6670..6daa156cef 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 46dbc86e1c..044cb79ae5 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 0000000000..744d57aa56 --- /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 e2c3a895b4..a2335444c6 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 e6e36c08ff..6b21b2230e 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 0000000000..9902569f70 --- /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 204/395] 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 2df0d87ef3..795e428466 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 205/395] 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 93349054a4..fa8897266e 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 0000000000..85f5a6e128 --- /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 0000000000..3ac113d6aa --- /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 0000000000..68e2b409b5 --- /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 0000000000..ec3e7c4d09 --- /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 0000000000..98648c2898 --- /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 0000000000..3a6df58d2c --- /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 206/395] 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 bfde59a309..19604f38cb 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 0000000000..88232251ce --- /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 0000000000..eb6daff565 --- /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 0000000000..0a79c8cdb1 --- /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 0000000000..aedd853fb4 --- /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 0000000000..cdbb8cfc46 --- /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 0000000000..094e9848c4 --- /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 0000000000..ee18f5f117 --- /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 0000000000..9622c5beba --- /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 207/395] 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 a691d856d5..a81348102b 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 208/395] 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 7e104c2f18..252e85c5a1 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 b02a0a843c..7e5e93e512 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 279e2fd66a..9793b2baaf 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 f4eea833a0..18f135cadb 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 272a4631c9..105187f49d 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 209/395] 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 5af1cad2f1..268f5592b5 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 97ee20d7b4..21c6128d95 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 24f2bf0190..5e2b0217c6 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 9a65d9a242..76d7b690c2 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 c804689615..fe5dc171e7 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 1f74cea429..fc432b5030 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 7f3910c158..6a32c0b997 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 61558f0080..7ab3281200 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 39b7207c03..9378ccbb69 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 d8ccc15b7b..307b3d5082 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 bd2a32c375..25ecac04a5 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 16db45b6c5..b44d736222 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 fcdbe86947..dd1aec3aa3 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 0000000000..89ee292b3b --- /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 bb3b06847d..97db186189 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 80e01b08c5..bfe4fc9f76 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 1c85a928ad..44416efc66 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 0000000000..d792f72d46 --- /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 0000000000..525dec86fc --- /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 0000000000..92715f7433 --- /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 0000000000..d86e34137c --- /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 0000000000..db9fc41685 --- /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 210/395] 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 a1b6736309..484b03e8ba 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 be9423442d..9f21791d6c 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 fd5b5d681d..10d83057f4 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 714bc09a53..ad318e6238 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 a616dbec20..948fa6359e 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 c2536ede81..a977693283 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 33b1ca38ec..85038d8c56 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 5b427a750f..95916ee9a5 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 875fb9e7a4..0ffaab4003 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 b4e3ac102d..80463fa6c3 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 214a2402e3..6aa8aef96d 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 71b9570b45..bb6ccf8747 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 0000000000..6e48bbc1cf --- /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 0000000000..dea911d1be --- /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 0000000000..c2677d0b96 --- /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 0000000000..180721bd68 --- /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 3d22b2360f..71126f8065 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 322832f9fa..1c0458d7e1 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 909412bc7a..b0ec0005b3 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 9bd5abf8c0..317ee124bb 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 35332d0ca4..6f3aae93a0 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 bb3b06847d..92bac77ae3 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 25183c4eea..3a1a6458d4 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 0000000000..6facf916c8 --- /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 75debe6155..4a53da4c02 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 0e0416e05a..623f60465b 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 0000000000..52930a02c3 --- /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 0000000000..c891a20849 --- /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 0000000000..b7dd210721 --- /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 0000000000..23b9e14115 --- /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 0000000000..2df20a0ede --- /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 0000000000..cb701dc936 --- /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 0000000000..cb701dc936 --- /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 0000000000..d2ac5db995 --- /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 c4013d2889..f143419dc9 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 211/395] 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 75e6ad50f3..1f66e566fc 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 1ca9313b71..2daabdb922 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 9902569f70..b4912c9f65 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 212/395] 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 223d455404..716d05a946 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 50b461614d..d4cdf0810a 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 2fea1ef2c7..67ff7d5285 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 213/395] 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 bf77001ee6..ee7435b2de 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 2618dacf9a..c8a8d6ba61 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 0000000000..32edfc4028 --- /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 0000000000..89c591dd78 --- /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 1c85a928ad..e4b66990d0 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 0000000000..a48fab894e --- /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 0000000000..c57433d57c --- /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 0000000000..b4a56f5c5d --- /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 0000000000..93da087d29 --- /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 214/395] 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 4488f90cbc..ba3b48b02d 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 9dd6bbcac4..df6dfc608c 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 354004e9ed..4d3904bde7 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 63e279d047..ddbcdd8da8 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 cdd00c4e7c..b9fc42392d 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 0000000000..bfd8913982 --- /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 0000000000..3239c26bfb --- /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 0000000000..41e60b09dd --- /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 0000000000..f71f3eb987 --- /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 215/395] 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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